본문 바로가기
항해99/실전 WIL | TIL

[TIL-007] 리액트 Hooks, Styled-component, 리액트 todo-list, 튜터님 질문 응답 정리

by junvely 2023. 4. 18.

Today목표 : 4/17일 리액트 Hooks, Styled-component, 리액트 todo-list, 튜터님 질문 응답 정리


알게된 점,

 

리액트 Hooks(useState, useEffect, useRef)

1. useState : state가 업데이트 될 때 마다 리렌더링

리액트에서 state를 업데이트 하는 방식 : 배치 업데이트 vs 함수 업데이트 차이

1) 배치 업데이트

<button
        onClick={() => {
          setCount(count + 5); 
          setCount(count + 1);
          setCount(count + 1); // 명령을 모아 가장 마지막만 반영되어 한번만 업데이트
        }}
      >

=> 명령의 변경 사항을 모아서 '한번만' 업데이트 한다.

 

2) 함수 업데이트

<button
        onClick={() => {
          setCount((currCount) => currCount + 1);
          setCount((currCount) => currCount + 3);
          setCount((currCount) => currCount + 1); //전부 모아서 한번씩 실행된 결과값을 한번에 반영
        }}
      >

=> 함수 업데이트는 항상 새로운 state를 받고(input), return 한다(output)

=> 명령을 모아서 순차적으로 한번씩 실행시킨다(3번 각각 실행 후 return).

 

3) 리액트에서 state를 배치 업데이트를 사용하는 이유 

렌더링이 잦다 = 성능에 이슈가 있는 것이다.

불필요한 렌더링을 피하기 위해 성능 최적화가 필요하다.

한꺼번에 명령을 모아서 한 번만 처리하는 것이 렌더링을 줄일 수 있기 때문이다.

 

 

2. useEffect , 의존성 배열(dependency array)

렌더링(mount, 리렌더링)될 때, 특정한 작업을 수행해야 할 때 설정하는 Hook

=> 화면이 보여졌을 때, 컴포넌트가 화면에서 사라졌을 때 실행

 

1. 의존성 배열 : 의존성 배열에 값을 넣으면 그값이 바뀔 때만 실행한다. 함수 뒤에 두번째 인자로 전달

1) 빈 배열 전달 => [ ] : 첫 mount 시에만 실행

2) 배열에 특정 값 전달 => [value] : 첫 mount시 + value가 업데이트 될 때마다 실행

3) 아무것도 전달x => 첫 mount시 + 리렌더링 시마다 실행

❗이전 프로젝트 때, state 값에 따라 실행하고 싶을 경우 useMemo를 사용해 해당 state가 변경될 때마다 실행되게 했었는데(회원가입 유효성 검사 onChange에서 사용), 둘의 차이점이 무엇인지 궁금하다. 현재로써 생각되는 차이점은 useEffect는 해당 value가 변경될 때 뿐만 아니라, mount될 시에도 실행되고, useMemo는 value가 변경 시에만 실행되기 때문에 회원가입 유효성 검사에서는 mount시 실행되면 안되기 때문에 값이 변경될 때만 실행되게 useMemo를 사용한 것 같은데 뒷 부분의 useMemo 사용 방법을 공부하며 이 부분의 차이점을 다시 한번 체크해 보아야 겠다.

 

2. clean up 기능 => useEffect 내부에서 return문 사용시, 컴포넌트가 화면에서 사라질 때 실행된다.

useEffect(()=>{
		// 화면에 컴포넌트가 나타났을(mount) 때 실행하고자 하는 함수를 넣어주세요.

		return ()=>{
			// 화면에서 컴포넌트가 사라졌을(unmount) 때 실행하고자 하는 함수를 넣어주세요.
		}
	}, [])
const 속세 = () => {
  const nav = useNavigate();

  useEffect(() => {
    return () => {
      console.log(
        "안녕히 계세요 여러분! 전 이 세상의 모든 굴레와 속박을 벗어 던지고 제 행복을 찾아 떠납니다! 여러분도 행복하세요~~!"
      );
    };
  }, []);

  return (
    <button
      onClick={() => {
        nav("/todos");
      }}
    >
      속세를 벗어나는 버튼
    </button>
  );
};

 

3. useRef : DOM 요소에 직접 접근할 수 있는 Hook

1. 저장 공간으로서의 Ref

const ref = useRef('초기값')
ref.current('변경값')

ref객체 안에 .current 키의 값으로 초기값이 들어간다. 키에 접근하여 값을 변경할 수 있다.

=> 설정된 ref값은 컴포넌트가 계속 리렌더링 되도 unmount 전 까지 값이 유지된다.

 

2. state와의 차이 

- state와 비슷하지만, state는 값 변경 시 렌더링 되며, 렌더링 시 내부 변수를 초기화 시킨다.=> 함수가 다시 그려짐

- ref에 저장된 값은 값이 변경되더라도 렌더링하지 않는다. 따라서 값 변경 시 내부 변수가 초기화 되지 않는다. = 함수가 그대로, 값을 초기화하지 않고 값만 변경됨 => 컴포넌트가 100번 리렌더링 되도 ref에 저장된 값은 유지된다.

ex) useSate = count + 1 시 값 증가 => 렌더링 => 컴포넌트에 바로 반영됨

useRef = count + 1 시 값 증가 => 렌더링x => 컴포넌트에서 여전히 초기값으로 보임 

=> ❗useRef로 값을 변경하여도 렌더링이 일어나지 않고, 렌더링 시에도 값이 초기화 되지 않는다. 여기서 일반 변수도 값이 변경되어도 리렌더링이 되지 않기 때문에 일반 변수와 변수에 useRef를 담아 사용하는 것의 차이점이 궁금했다. 일반 변수에 담으면 렌더링 시 초기화 되는가? 어떨 때 사용되는지 궁금하다.

=> ✅ 리액트에서 변수와 useRef는 모두 값을 저장하는 데 사용되지만, 사용 방법과 동작 방식에 차이가 있습니다.

변수는 일반적으로 컴포넌트의 state나 props에서 사용할 값들을 저장하는 데 사용됩니다. 변수는 컴포넌트 내부에서 정의되며, 값을 변경하면 컴포넌트가 다시 렌더링됩니다. 이러한 특성 때문에 변수는 컴포넌트의 state를 관리하는 데 자주 사용됩니다.

반면 useRef는 컴포넌트에서 DOM 요소나 다른 값을 저장하는 데 사용됩니다. useRef를 사용하면 컴포넌트가 다시 렌더링되더라도 값이 변경되지 않습니다. useRef로 저장된 값은 컴포넌트의 lifecycle과는 관계 없이 계속 유지됩니다.

또한 useRef는 DOM 요소에 대한 참조를 생성하는 데에도 사용됩니다. 이 경우 useRef를 사용하여 생성된 참조는 실제 DOM 요소를 참조하는 JavaScript 객체를 반환합니다. 이는 컴포넌트에서 DOM 요소를 조작하는 데 매우 유용합니다.

따라서 변수와 useRef는 각각 다른 용도로 사용되며, 언제 어떤 것을 사용할지는 사용자의 상황과 필요에 따라 달라질 수 있습니다.

 

2. DOM요소 접근 방법으로서의 useRef

1) useRef 변수명을 태그에서 ref 속성으로 연결

2) ref.current.focus()와 같이 메소드를 사용해 DOM요소를 조작할 수 있다. 

=> ❗ref로 DOM을 직접 조작하는 것과(focus), event target으로 조작하는 것, state를 통한 값 변경으로 조작하는 것의 차이점이 궁금하다. 렌더링에서 차이가 있는 것인가? 어떤 방법이 성능에 더 좋은가?의 차이점이 궁금하다.

 

 

Styled Component 사용방법

Styled Component는 props를 통해서 부모 컴포넌트로부터 값을 전달받고, 조건문을 이용해서 조건부 스타일링을 할 수 있다.

1. 설치

yarn add styled-components

- 주의 : component와 components가 다르다... components로 설치하고 import시에도 s조심할것

"styled-component": "^2.8.0",
"styled-components": "^5.3.9",

 

2. 확장프로그램 설치 => vscode styled component

3. import하여 적용하기

// styled-components에서 styled 라는 키워드를 import 합니다.
import styled from "styled-components";

// styled키워드를 사용해서 styled-components 방식대로 컴포넌트를 만듭니다. 
const StBox = styled.div`
	// 그리고 이 안에 스타일 코드를 작성합니다. 스타일 코드는 우리가 알고 있는 css와 동일합니다.
  width: 100px;
  height: 100px;
  border: 1px solid red;
  margin: 20px;
`;

const App = () => {
	// 그리고 우리가 만든 styled-components를 JSX에서 html 태그를 사용하듯이 사용합니다.
  return <StBox>박스</StBox>;

=> styled. 뒤에는 html의 태그가 온다. 내가 원하는 html 태그를 사용해서 styled-components를 만들 수 있다.

4. 장점 :  CSS in JS 방식이기 때문에, if문이나 삼항 연산자나 배열, 함수 등을 사용할 수 있다.

// 1. styled-components를 만들었습니다.
const StBox = styled.div`
  width: 100px;
  height: 100px;
  border: 1px solid ${(props) => props.borderColor}; // 4.부모 컴포넌트에서 보낸 props를 받아 사용합니다. 
  margin: 20px;
`;

const App = () => {
  return (
    <div>
	{/* 2. 그리고 위에서 만든 styled-components를 사용했습니다. */}
	{/* 3. 그리고 props를 통해 borderColor라는 값을 전달했습니다. */}
      <StBox borderColor="red">빨간 박스</StBox>
      <StBox borderColor="green">초록 박스</StBox>
      <StBox borderColor="blue">파랑 박스</StBox>
    </div>
  );
};

switch문과 map을 이용한 리팩토링

import styled from "styled-components";

const StContainer = styled.div`
  display: flex;
`;

const StBox = styled.div`
  width: 100px;
  height: 100px;
  border: 1px solid ${(props) => props.borderColor};
  margin: 20px;
`;
// 박스의 색을 배열에 담습니다.
const boxList = ["red", "green", "blue"];

// 색을 넣으면, 이름을 반환해주는 함수를 만듭니다.
const getBoxName = (color) => {
  switch (color) {
    case "red":
      return "빨간 박스";
    case "green":
      return "초록 박스";
    case "blue":
      return "파란 박스";
    default:
      return "검정 박스";
   }
};
const App = () => {
  return (
    <StContainer>
			{/* map을 이용해서 StBox를 반복하여 화면에 그립니다. */}
      {boxList.map((box) => (
        <StBox borderColor={box}>{getBoxName(box)}</StBox>
      ))}
    </StContainer>
  );
};

=> ❗스타일을 상황에 따라 여러가지로 변경해야 할 경우 배열이나 함수를 이용해 적용할 수 있어 유용할 것 같다. SCSS에서 이런 경우 어떻게 사용할지 고민되어 className에 삼항연산자나 &&를 사용하여 추가해 주었는데, 가독성에서 많이 좋지 않아 보였다. 이런 경우는 styled-component로 사용하면 좋을 것 같은데 Button같이 스타일이 다르게 적용되야 할 컴포넌트에서는 이런 경우만 둘을 같이 병행해도 괜찮은지 궁금했다.

 

리액트 todo-list 완성

1. 하위 컴포넌트에 state를 전달해서 값을 변경(업데이트)하는 방법

 하위 컴포넌트 Input에 state와 setState를 전달해서 입력값으로 업데이트 하기

// state, setState 전달
<Input type="title" value={title} setValue={setTitle} addTodo={addTodo}>
// setState에 입력값으로 업데이트
<input value={value} onChange={(event) => setValue(event.target.value)}></input>

 

2. TODO - LIST 완성모델의 코드와 내 코드 비교 및 차이점

1)  완성모델 : 페이지 생성 후 진행

=> ❗나는 간단한 작업이라 바로 app 컴포에서 진행했지만 실제로는 페이지를 생성해 진행하기 때문에 이 방법이 옳은 것 같다.

 

2) 완성모델 : todos, setTodos를 하위 컴포넌트에 전달하여, 하위 컴포넌트에서 setTodos를 업데이트 하는 함수를 관리하고, 업데이트 시킨다.

=> 나는 최상위 app컴포넌트 에서 add, delete, update하는 로직을 관리하고, 하위 컴포넌트에 필요 함수를 전달 => 하위 컴포넌트에서 함수에 매개변수로 변경값을 전달받아 app컴포넌트의 함수가 실행되게 하였는데,

이렇게 하면 1.어떤 기능들이 있는지 상위 컴포넌트에서 한번에 확인하기 쉽다는 장점이 있지만, 1. 앱의 복잡도가 증가할 수록 상위 컴포넌트의 코드가 난잡해 질 수 있다는 단점과, 2. 하위 컴포넌트로 함수를 계속해서 전달해야 한다는 점, 3. 하위 컴포넌트에서는 어떤 기능을 수행하는지 파악하기 힘들다는 단점이 있다.

비교해보니 완성작에서는 state와 setState만 전달하고, 업데이트 하는 함수 로직은 관련된 컴포넌트에서 관리하였다. 이렇게 하면 1. state와 setState만 넘겨주면 되고, 하위 컴포넌트에서 계속해서 불필요한 함수를 전달을 목적으로 props으로 내려주는 props drilling을 하지 않아도 된다. 2. 앱의 복잡도가 증가하여도 app 컴포넌트는 난잡해지지 않고 꼭 필요한 함수만 가지고 있다. 3. 하위 컴포넌트가 어떤 기능을 수행하는지 파악하기 쉽다. 

이와 같은 차이점으로 보아, 앞으로 계속 복잡한 앱을 효율적으로 관리하기 위해서는 완성작과 같이 불필요한 함수를 계속해서 하위 컴포넌트의 props으로 내려주지 않고, setState만 내려주어 해당 컴포넌트에서 함수를 생성해 업데이트 하도록 하는 것이 좋을 것 같다. props으로 drilling 현상으로 에러 발생 시 어디에서 문제가 있는지 확인이 어렵고, 어디서 props을 내려주고 있는 것이지 파악하기 어렵기 때문이다. 저번 시간에 배웠던 내용이라 더 기억에 남는 것 같았다.

=> 따라서 종합적으로 state와 함수 로직을 최상위 컴포넌트에서 관리해야 하는지 하위 컴포넌트에서 관리해야 하는지에 대한 나의 결론은, 최상위 컴포넌트에서 관리해야 할 state나 함수 로직은 => 여러개의 하위 컴포넌트에서 공통적으로 사용, 업데이트 되어야 하는 state나 함수는 app이나 최상위에서 관리하여 하위 컴포넌트로 전달하고, 그 외의 state나 함수 로직은 관련 하위 컴포넌트에서 관리하여 전달받은 setState로 업데이트 해야 한다는 생각이다. 이는 계속해서 프로젝트를 경험해 보면서 감을 익히다 보면 자연스럽게 컴포넌트를 짜는 능력이 늘 것 같다는 생각이 들었다.

 

3) 여러 개의 입력값을 받아야 하는 경우, 예를들어 title값과 text값을 input으로 받아 업데이트 해야 할 경우, 입력값을 어떻게 업데이트 함수에 전달할 것인가에 대해 고민했었다.

나는 처음에 하나의 state를 객체로 생성하여, 각각 input에 onChange 이벤트가 일어날 때 입력값을 state안의 title과 text key의 값이 변경되도록 하였다. 그러나 여기서 문제점이, state 값이 변경될 때마다 리렌더링이 일어나 다른값은 초기화 되고 해당 값만 업데이트 되는 문제가 발생했다. title을 변경하면 text가 초기화되고, text를 입력하면 title이 초기화되었다. => 이 문제의 해결방법으로 각각 title과 text의 state를 각각 따로 만들어 버튼 클릭 시 여러값들을 함수에 전달해 주었다.

=> 완성작에서 확인해 보니, 이런 문제점들을 보완하기 위해 state가 아닌 변수의 객체로 생성하여 변경값을 전달 받았다. 때문에 onChange 이벤트로 객체의 정보가 업데이트 되더라도 state가 아니기 때문에 리렌더링이 일어나지 않아 모든 값이 전부 안전하게 객체에 할당되었고, 해당 객체를 사용하여 업데이트 함수에서 setState로 업데이트 되도록 설계되어 있었다. => ❗여기서 위와 같은 의문이 들었던게 리렌더링이 일어나지 않는 다는 점을 이용한다면 변수 대신 useRef를 이용해도 될 것 같은데, useRef는 객체이고 .current 속성 안에 값이 저장되기 때문에 굳이 객체 안의 속성 값으로 객체를 넣어 사용할 필요는 없기 때문에 변수를 사용하는게 맞는 것 같다.

결론적으로 initial 객체가 도대체 왜 있는건지, 왜 사용하라는 건지 의문이었는데, 이렇게 초기 객체를 만들어 놓고 값을 업데이트 하여 사용하기 위해서였다.. state가 아니기 때문에 리렌더링도 일어나지 않고, 객체로 한번에 모든 값들을 다 보낼 수 있다. 굉장히 중요한 부분을 배운 것 같다. 다음부터는 꼭 기억해 두어야 겠다. 

 

 

튜터님과의 면담 질문/응답

 

1. 컴포넌트를 잘짠다 = 재사용성을 높게 만든다?, 컴포넌트를 잘 만든다는 기준은 무엇인가?

- 컴포넌트의 공통화, 재사용성을 높게하여 렌더링을 최소화 = 컴포넌트를 최적화 시키는 것

: state의 값 변경 = rendering => 불필요한 렌더링이 많을 수록 화면에 계속해서 그려져야 하는 만큼 1. 리소스를 많이 씀, 2. 사용자 불편성(깜빡임)이 증가한다. 

따라서 불필요한 렌더링을 줄이기 위하여 변하지 않는 컴포넌트는 렌더링을 하지 않도록 렌더링을 최소화(memo 등)하여 컴포넌트를 최적화 시키는 것이 컴포넌트를 잘 만드는 목적이라고 할 수 있다.

 

2. state와 함수를 app 컴포넌트에서 관리해야 하는가? 하위 컴포넌트에서 관리해야 하는가?

:  상황에 따라 다르다. 위와 같은 목적을 기준으로 필요한 곳에서 리렌더링을 고려하여 적절하게 사용해야 한다.

 

 

내일 목표 ,

1. React Hooks 남은 hook들까지 공부 및 완벽 이해하기

2. VDOM에 대한 이해 및 지금까지 React 개념 복습 및 정리하기

3. Life cycle에 대한 이해