..

Search

14) State

State


State

React에서는 props와 state라는 객체를 가지고 데이터를 다루게 됩니다. 두 객체 모두 View를 렌더링하는데 사용되는 데이터를 가지고 있다는 공통점을 가지고 있지만, 한 가지 중요한 차이점이 있습니다.

 

props는 함수의 매개변수처럼 부모 컴포넌트로부터 데이터를 전달받지만, state는 함수 내에서 선언된 변수처럼 컴포넌트 내에서 관리된다는 점입니다. 따라서 컴포넌트에서는 props의 값을 변경할 수 없지만, state의 값은 변경할 수 있게 됩니다.

 

React v16.8 이전까지는 함수 컴포넌트에서는 state를 사용할 수 없었습니다. 따라서 state를 사용하기 위해서는 어쩔 수 없이 클래스 컴포넌트를 사용해야 했지만, React v16.8 부터 도입된 useState Hook을 사용하면 함수 컴포넌트에서도 state를 사용할 수 있게 되었습니다.

 

react-data-flow


useState Hook으로 state 관리하기

지금까지 우리는 사용자와의 상호작용이나 시간에 따라 일부 데이터가 변하는 동적인 부분이 전혀 없는 예제들만을 다루었습니다.

이번에는 state의 필요성을 알아보기 위해 버튼을 클릭하면 화면의 숫자가 증가하는 카운터 예제를 만들어보도록 합시다.

예제(Counter.js)
import { useState } from "react";

const Counter = () => {
  // 0을 초기값으로 하는 state 생성
  const [state, setState] = useState(0);

  return (
    <div>
      <h1>State 값 : {state}</h1>
      {/* setState를 사용하여 state의 값을 1씩 증가시킴 */}
      <button onClick={() => setState(state + 1)}>1씩 증가</button>
    </div>
  );
};

export default Counter;

함수 컴포넌트에서는 useState Hook을 사용하여 state를 손쉽게 관리할 수 있습니다.

Hook은 React에서 제공하는 특별한 기능을 수행하는 함수들이며, 나머지 Hook에 대한 내용은 8장에서 좀 더 자세히 알아보도록 하겠습니다.

 

React Hook 수업 확인 ⇒

 

함수 컴포넌트에서 useState Hook을 사용하는 문법은 다음과 같습니다.

useState 문법
const [state, setState] = useState(initialState);

 

useState는 맨 처음 렌더링을 수행할 때 초기 상태 값(initialState)을 인수로 전달받고, 최신 상태를 유지하는 값(state)과 그 값을 업데이트하는 함수(setState)를 반환합니다. 이때 우리가 앞서 살펴 본 구조 분해 할당 구문을 사용하여 배열의 값을 각각의 변수에 나누어 저장하고 있습니다. 이렇게 useState를 통해 반환된 첫 번째 값인 state 변수에는 항상 최신 상태의 state 값이 저장되게 됩니다.

 

예제에서 사용된 state와 setState라는 이름은 단순한 예제일 뿐입니다. 여러분이 useState를 사용할 때는 state에 저장될 데이터를 직관적으로 파악할 수 있는 이름을 대신 사용할 것을 적극 권장합니다.
예시

const [count, setCount] = useState(0);

 

11번째 라인에서는 setState() 함수에 새로운 state 값을 인수로 전달하여 state 객체를 업데이트하고 있습니다.

Counter.js
<button onClick={() => setState(state + 1)}>Click me</button>

 

setState() 함수는 state 객체의 변경 사항을 대기열에 저장하고, 해당 컴포넌트와 그 자식 컴포넌트들이 업데이트된 state의 값을 사용하여 리렌더링되어야 함을 React에게 알립니다. 이 함수는 이벤트 핸들러와 서버 응답에 따라 UI를 갱신할 때 가장 많이 사용하는 함수입니다. 따라서 9번째 라인의 코드는 state의 값을 1 증가시키고, Counter 컴포넌트를 리렌더링하라는 의미를 가집니다.

 

11번째 라인에서 사용된 onClick은 React에서 사용자의 마우스 클릭 이벤트를 처리하기 위해 사용하는 이벤트 핸들러입니다. 여러분은 6장에서 React에서 이벤트를 처리하는 방법에 대해 배우게 됩니다.

여러 개의 state 관리하기

상황에 따라 하나의 컴포넌트 내에서 여러 개의 state를 생성하고 관리해야 할 경우가 생길 수 있습니다. 이때 우리는 두 가지 방법 중 하나를 선택할 수 있습니다.

 

첫 번째 방법은 객체와 배열을 활용하여 서로 연관되는 데이터들을 하나의 state 변수로 묶어서 관리하는 방법입니다. 이 방법을 사용하면 하나의 state 변수로 관련된 데이터를 한 번에 모두 모아서 관리할 수 있습니다.

객체를 활용한 state 예시
const [area, setArea] = useState({
  left: 0,
  top: 0,
  width: 200,
  height: 100
});

 

하지만 이 방법을 사용하면 하나의 state 값만을 변경해야 할 경우에도 나머지 state 값들을 잃지 않기 위해서 모두 같이 업데이트 해줘야만 합니다.

 

다음 코드는 area라는 state의 left 값만을 50으로 업데이트하는 코드입니다. left 값만을 업데이트하면 되지만 다른 값들을 잃지 않기 위해 아래 코드와 같이 모두 같이 업데이트해야만 합니다.

left 값을 50 증가하는 코드
setArea({ left: 50, top: 0, width: 200, height: 100 });

 

두 번째 방법은 각각의 독립된 여러 개의 state 변수로 나누어 관리하는 방법입니다.

여러 개의 state로 나누어 관리하는 예시
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);
const [width, setWidth] = useState(200);
const [height, setHeight] = useState(100);

 

[state, setState]의 형태로 여러 개의 state 변수를 선언하게 되면, 변수의 이름을 서로 다르게 선언할 수 있기 때문에 각각의 변수를 개별적으로 업데이트 할 수 있습니다. 또한, 나중에 관련 state 로직이 복잡해지면 사용자 정의 Hook을 사용하여 손쉽게 해당 로직을 별도의 파일로 추출할 수 있습니다.

 

일반적으로는 이 두 가지 방법을 혼용하여 서로 관련 있는 state를 몇 개의 독립된 state 변수로 그룹화하여 관리하는 것이 코드의 가독성이나 효율성 측면에서 좋은 방안이 될 수 있습니다.

두 가지 방법의 혼용 예시
const [position, setPosition] = useState({ left: 0, top: 0 });
const [size, setSize] = useState({ width: 200, height: 100 });

 

나중에 프로젝트의 규모가 커져서 state와 관련된 로직이 복잡해지면 Reducer나 사용자 정의 Hook 등을 활용하여 state를 관리하는 것이 좋습니다.


state를 직접 수정해서는 안됩니다.

React에서 state를 사용할 때에는 몇 가지 주의해야 할 사항들이 있습니다.

 

우선 state의 값을 바꿀 때에는 state의 값을 직접 수정해서는 안되며, useState를 통해 반환된 setState() 함수를 사용하여 수정해야 합니다.

잘못된 코드
const [left, setLeft] = useState(0);
//...
left += 20;
올바른 코드
const [left, setLeft] = useState(0);
//...
setLeft(left + 20);

 

또한, state의 값이 객체나 배열인 경우에는 setState() 함수를 사용하여 해당 state의 값을 곧바로 변경해서는 안됩니다.

 

React에서는 참조 타입인 객체나 배열의 경우 불변성(immutability)을 지켜야만 합니다. 즉, 객체나 배열을 직접 수정해서는 안된다는 의미이며, 해당 객체를 업데이트하기 위해서는 원하는 값으로 새로운 객체를 만들어 덮어쓰는 방식으로 업데이트해야만 합니다.

 

이렇게 객체나 배열의 불변성을 지켜야 하는 이유는 바로 렌더링에서의 최적화 방식 때문입니다. React에서는 부모 컴포넌트가 업데이트될 경우에 해당 컴포넌트의 자식 컴포넌트들도 모두 함께 리렌더링 됩니다. React의 Virtual DOM은 특정 컴포넌트의 업데이트 필요성을 컴포넌트가 가지고 있는 이전의 state 값과 새로 업데이트된 state 값을 비교하여 판단하게 됩니다.

 

즉, 불변성이 지켜지지 않는다면 객체나 배열의 내부 데이터가 변경되어도 React는 해당 데이터가 바뀐 것을 감지하지 못하게 되는 것입니다.

 

따라서 ES6 문법부터 제공되는 spread 연산자(…)를 사용하여 기존의 객체를 새로운 객체로 먼저 복사한 다음에 특정 state 값을 업데이트하고 setState() 함수를 통해 새로운 state 값으로 업데이트해야만 합니다.

 

다음 코드는 size 객체의 모든 값을 복사하여 새로운 copy 객체를 생성하는 코드입니다.

Spread 연산자
const copy = { ...size };

 

다음 예제는 useState에 너비와 높이 데이터를 동시에 저장하는 객체를 전달하여 활용하는 예제입니다.

예제(Area.js)
import { useState } from "react";

const Area = () => {
  const [size, setSize] = useState({ width: 200, height: 100 });

  return (
    <div>
      <h1>
        너비 : {size.width}, 높이 : {size.height}
      </h1>
      <button
        onClick={() => {
          const copy = { ...size };
          copy.width += 20;
          setSize(copy);
        }}
      >
        너비 증가
      </button>
      <button
        onClick={() => {
          const copy = { ...size };
          copy.height += 10;
          setSize(copy);
        }}
      >
        높이 증가
      </button>
    </div>
  ); };

export default Area;


state의 업데이트는 비동기적입니다.

React는 인지 성능(perceived performance)의 향상을 위해 setState() 함수의 실행을 미루거나 여러 컴포넌트를 일괄적으로 업데이트 할 수 있습니다. 즉, setState() 함수는 컴포넌트를 항상 즉각 업데이트하는 것은 아니라는 점을 기억해야 합니다.

 

이와 같은 특성으로 인해 setState() 함수를 호출하자마자 state 객체에 접근하는 것은 잠재적으로 문제의 원인이 될 수 있습니다.

잘못된 코드
import { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setState(count + 1);
  };

  const manyIncrementCount = () => {
    incrementCount();
    incrementCount();
    incrementCount();
  };

  return (
    <div>
      <h1>Count 값 : {count}</h1>
      <button onClick={manyIncrementCount}>3씩 증가</button>
    </div>
  );
};

export default Counter;

 

위의 예제는 버튼을 누르면 count라는 state 값이 3씩 증가할 것이라고 생각하고 구현한 코드입니다. 하지만 리렌더링이 끝난 후 화면에 표시된 count 값은 1씩 증가하는 것에 그치게 됩니다.

 

React는 컴포넌트가 리렌더링될 때까지 count 값을 갱신하지 않기 때문에 incrementCount() 함수를 호출할 때마다 매번 count 값을 0으로 읽어들인 뒤 count 값을 1 증가한 값으로 설정하는 것입니다.

 

따라서 setCount() 함수가 언제나 가장 최신의 state 값을 사용하도록 보장하기 위해서는 다음 예제처럼 setCount() 함수를 호출할 때 state 객체 대신 함수를 인수로 전달해야만 합니다.

예제(Counter.js)
const incrementCount = () => {
  setState((count) => {
    return count + 1;
  });
};


연습문제