반디북
리액트 훅을 활용한 마이크로 상태 관리
Ch1
준영

01. 리액트 훅을 활용한 마이크로 상태 관리

책에서 말하는 리액트 훅 등장 이전, 즉, 클래스형 컴포넌트 위주의 개발 방식에서는 상태 객체 하나를 통해 중앙에서 관리하므로 중앙 집중적인 방식이라고 말하는 것 같다.
하지만 함수형 컴포넌트가 등장한 후, useState와 같은 리액트 훅을 통해 필요에 따라 상태를 정의하거나, 재사용 가능한 훅을 정의하는 등 사용자가 목적에 맞게 사용하는 트렌드로 변화되었다.
✅ 따라서 이렇게 변화된 방식을 중앙 집중적인 방식에서 벗어나 더 목적 지향적이며 특정한 코딩 패턴과 함께 사용되는 마이크로 상태 관리라고 언급하는 듯하다.

마이크로 상태 관리 이해하기

'상태(state)'란?

사용자 인터페이스(UI)를 나타내는 모든 데이터. 즉, 렌더링에 필요한 데이터.

기본적으로 상태 관리에 필요한 기능은 다음과 같다.

  • 상태 읽기
  • 상태 갱신
  • 상태 기반 렌더링

그리고, 추가적으로 필요할 수 있는 기능은 다음과 같다.

  • 리렌더링 최적화
  • 다른 시스템과의 상호 작용
  • 비동기 지원
  • 파생 상태
  • etc..

이 모든 기능들이 필요한 것은 아니며 목적에 맞게 사용자가 정의하여 사용해야 한다.

리액트 훅 사용하기

useState

  • 지역 상태를 생성하는 가장 기본적인 함수

useReducer

  • useState와 마찬가지로 지역 상태를 생성할 수 있는 함수
  • useState를 대체하는 용도로 사용한다고 한다.

useEffect

  • 리액트 렌더링 바깥에서 로직을 실행하기 위한 함수

이외에도 여러 기본 리액트 훅이 있지만, 공식 문서를 참고하자.

리액트 훅을 통해 컴포넌트에서 로직을 추출해보자.

const useCount = () => {
  const [count, setCount] = useState(0);
  /* 필요한 로직을 해당 부분에 추가할 수도 있다! */
  return [count, setCount];
}
 
const component = () => {
  const [count, setCount] = useCount();
 
  return ~~~;
}

위의 예시는 로직을 추출한 것이 오히려 더 복잡하고 불필요해보일 수 있다.
간단한 상태야 useState를 통해 필요에 따라 선언하여 사용할 수 있지만, 복잡해질 경우 해당 상태를 관리하기 위한 코드가 길어져 가독성을 떨어뜨릴 수 있다.
따라서 위와 같이 분리하여 얻을 수 있는 장점은 다음과 같다.

  • 해당 컴포넌트에서 사용하는 상태의 의미를 이름을 통해 명확하게 유추할 수 있도록 하여 가독성을 높인다.
  • 상태가 컴포넌트에서 분리되어 컴포넌트를 건드리지 않고 기능을 추가할 수 있고, 뷰에 집중할 수 있다.

💡 이렇게 정의한 커스텀 훅은 단순히 작은 기능을 추가하는 Wrapper가 될 수도 있고 큰 역할을 하는 거대한 훅이 될 수도 있다.

데이터 불러오기를 위한 서스펜스와 동시성 렌더링

Suspense

  • 비동기 처리에 대한 걱정 없이 컴포넌트를 코딩할 수 있는 방법

동시성 렌더링

  • 렌더링 프로세스를 chunk 단위로 분할하여 CPU utilization을 높이는 방법

전역 상태 탐구하기

지역 상태

  • 컴포넌트에서 정의되고, 컴포넌트 트리 내에서 사용되는 상태

전역 상태

  • 어플리케이션 내 멀리 떨어진 여러 컴포넌트 사이에서 사용할 수 있는 상태
  • 싱글턴일 필요는 없다.

리액트는 컴포넌트 모델에 기반하기 때문에 지역성이 매우 중요하다.
따라서 컴포넌트가 서로 격리되어야 하고, 재사용이 가능해야 한다.

useState 사용하기

useState는 상태와 함께 상태를 업데이트하는 setState 함수를 제공하며 상태의 값을 갱신할 수 있다.

const [count, setCount] = useState(0);
 
/* ~~~ */
 
<div>
  {count}
  <button onClick={() => setCount(1)}>
    /* ~~~ */
  </button>
</div>

위의 예시의 경우, 버튼을 처음 클릭하였을때 count라는 상태가 0에서 1로 갱신되며 상태가 변하여 리렌더링이 일어난다.
하지만 이후 여러 번을 클릭하더라도 이미 1인 count를 동일한 1로 갱신하려고 하기 때문에 리렌더링이 일어나지 않는다.
이를 베일아웃(bailout)이라고 부르며, 상태 값의 변경이 이루어지지 않아 리렌더링을 발생시키지 않는 것을 의미한다.

const [state, setState] = useState({ count: 0 });
 
/* ~~~ */
 
<div>
  {count}
  <button onClick={() => setState({ count: 1 })}>
    /* ~~~ */
  </button>
</div>

하지만 예시를 위와 같이 변경할 경우, 버튼을 누를때마다 리렌더링이 발생한다.
객체는 immutable하기 때문에 새로운 객체가 생성되어 이전 객체와 동일하지 않다고 판단되어 베일아웃이 일어나지 않기 때문이다.

또 다른 예시로 버튼 클릭시 count가 증가하는 예시를 살펴보자.

const [count, setCount] = useState(0);
 
/* ~~~ */
 
<div>
  {count}
  /* 1. */
  <button onClick={() => setCount(count + 1)}>
  /* 2. */
  <button onClick={() => setCount((c) => c + 1)}>
    /* ~~~ */
  </button>
</div>

1번과 2번은 비슷해 보이지만 동작이 다르다.

1번은 버튼을 빠르게 클릭할 경우, 클릭한만큼 count가 올라가지 않을 수 있다.
하지만 2번은 빠르게 클릭하더라도 클릭한만큼 count가 올라간다. 이전 상태 값에 기반하여 상태 업데이트가 이루어지기 때문이다.

✅ 1번의 상태 업데이트가 제대로 이루어지지 않는 이유를 순간 race condition 때문인가 생각하였지만, batch update 때문인것 같다.
리액트는 상태 변경에 대해 렌더링을 최적화하기 위해 상태 변경을 batch로 모아서 처리하기 때문이다.
따라서 1번의 경우, 아직 상태 변경이 이루어지지 않은 count 값을 여러 번 불러와 상태 업데이트가 제대로 이루어지지 않았다고 생각했기 때문이다.
반면, 2번의 경우, 이전 상태에 기반하여 업데이트가 이루어져 batch로 모아 처리하더라도 정상적으로 동작하는 것이라고 생각한다. (맞나...?)

useReducer 사용하기

const reducer = (state, action) => {
  switch (action.type) {
    case '~~~' :
      return ~~~~;
    case '...' :
      return ...;
    /* ... */
  }
}
 
const Component = () => {
  const [state, dispatch] = useReducer(reducer, { ~~~ });
 
  return ...;
}

사용 방법은 다음과 같고, dispatch({ type: ACTION_NAME })과 같이 dispatch 함수를 통해 미리 정해둔 액션을 통해 상태를 갱신할 수 있다.
미리 액션과 업데이트될 내용을 정의해두기 때문에 코드를 분리할 수 있고, 테스트 용이성 측면에서 이점이 있다.

useState와 useReducer의 유사점과 차이점

useStateuseReducer는 서로를 통해 구현 가능하고, 특별한 경우를 제외하고 거의 모든 경우 서로를 대체 가능하다.
따라서 사용자의 선호도와 프로그래밍 스타일에 따라 둘 중 하나를 편하게 사용하면 된다!

✅ 그럼 언제 useState를 사용하고, useReducer를 사용해야할까?

GPT 선생님 말로는 다음과 같다.

[useState]

- 간단한 컴포넌트나 상태가 단순한 경우에 유용합니다.
- 단일 값 또는 객체를 통해 상태를 관리할 수 있습니다.
- 간단한 상태 갱신 로직을 가질 때 사용하기 좋습니다.

[useReducer]

- 복잡한 상태 또는 상태 갱신 로직이 복잡한 경우에 적합합니다.
- 여러 상태를 함께 관리하거나 상태 갱신 로직이 복잡한 경우에 효율적입니다.
- 상태를 변경하는 로직이 복잡하거나 컴포넌트의 성능 최적화를 위해 사용될 때 적합합니다.

사실 useState만을 사용해도 큰 무리가 없을 것이라는 생각이 들기는 한다. 로직이 커져도 훅으로 분리하여 정의하면 되니... 또 간편한 사용성 때문에 useReducer보다 useState가 선호될 것 같다.
하지만 useReducer를 사용한다면, 상태가 변경되는 상황이 정해져있고, 복잡할 경우에 사용할 것 같다는 생각이 든다.

도은

마이크로 상태 관리 이해하기

💡 React에서 상태는?
   사용자 인터페이스(UI)를 나타내는 모든 데이터를 말한다.

기본적인 상태 관리

  • 상태 읽기
  • 상태 업데이트
  • 상태 기반 렌더링

추가적인 기능

  • 리렌더링 최적화
  • 다른 시스템과의 상호 작용
  • 비동기 지원
  • 파생 상태

리액트 훅 사용하기

const useCount = () => {
  const [count, useCount] = useState(0);
  return [count, useCount];
};
 
const Component = () => {
  const [count, useCount] = useCount();
 
  return ...
};

👆 크게 달라질 것이 없기 때문에 불필요하게 복잡해졌다고 생각할 수 있다.

다음 두 가지 관점을 생각해보자.

  • useCount라는 이름을 통해 더 명확해졌다.
    • 사용자 정의 훅 → 코드 가독성 향상
  • ComponentuseCount 구현과 분리되었다.
    • 컴포넌트를 건드리지 않고도 기능을 추가할 수 있다.

이처럼, 다양한 목적에 맞는 사용자 정의 훅을 제공할 수 있다.

💡 단순히 작은 기능을 추가하는 Wrapper가 될 수 있고
   더 큰 역할을 하는 거대한 훅이 될 수 있다.

전역 상태 탐구하기

💡 컴포넌트 자체는 전역 상태에 가급적 의존하지 않는 것이 좋다.

👆 위의 문장 곱씹어보기

  • 컴포넌트는 지역성이 중요하다.
    • 독립적이여야 하고, 재사용이 가능해야 한다는 것을 의미
    • 외부에 의존하는 경우, 동작이 일관되지 않거나 재사용이 불가능할 수 있다.

🤔 컴포넌트가 외부에 의존하여 재사용이 불가능한 경우

const Foo = () => {
  const { bar } = useContext(BarContext);
 
  return ...
};

위 컴포넌트를 재사용하기를 원한다면 <BarContext.Provider>를 반드시 상위에 감싼 후 사용해야 한다.

그렇지 못할 경우 의도대로 동작하지 않을 것..😓

useState 사용하기

<button onClick={() => setCount(1)}>

👆 버튼을 여러 번 클릭하면 setCount(1)을 다시 호출하지만, 동일한 값이기 때문에 '베일아웃'되어 리렌더링 X

💡 베일아웃
   리렌더링을 발생시키지 않는 것

🤔 아래 코드의 차이점은 뭘까?

// 1
<button onClick={() => setCount(count + 1)}>
 
// 2
<button onClick={() => setCount(c => c + 1)}>
  • 1은 카운트가 증가
  • 2는 실제 버튼을 클릭한 횟수를 센다.

따라서, 1의 경우 여러 상태 업데이트가 동시에 일어날 경우 예상치 못한 결과를 초래할 수 있다.

2의 경우, 상태 업데이트가 이전 상태에 기반하여 이루어진다는 점에서 연속적인 상태 업데이트가 발생할 경우에도 각 업데이트가 이전 업데이트의 결과에 기반하기 때문에, 상태 업데이트가 정확하게 반영된다.

useReducer와 useState

💡 기본적으로 동일하며 상호 교환 가능
   → 각자 선호도나 프로그래밍 스타일에 따라 둘 중 하나를 선택하면 된다.

🤔 그런데 왜 우리는 useState를 더 많이 사용하는 것일까?

  • 간결하고, 대부분의 컴포넌트 상태 관리 요구사항은 useState로 충분히 해결 가능
  • 러닝 커브도 낮다.

🤔 useReducer가 더 적합한 경우?

  • 결국에는 둘 다 상태관리를 위한 훅이라서
  • useState로 해결할 수 없는 상황은 드물다.
  • gpt 왈.. 복잡한 상태 관리에서 useReducer가 더 적합할 수 있다고..
  • 개인적인 생각, 케이스 별로 상태 관리를 해야 한다면 useReducer가 적합할 수도..?
    • 러닝 커브 대비하면, useState로도 충분히 커버 가능할 거 같기도