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

전역 상태 관리 문제 해결하기

전역 상태는 컴포넌트에 대한 추가적인 의존성이 필요하기 때문에 되도록 피하는 것이 좋다.
하지만, 전역 상태를 사용하면 매우 편리하고 생산성을 높일 수 있다.

✅ 따라서 필요에 따라 적절한지를 판단하고 잘 설계해서 사용하면 될 것 같다!

전역 상태의 설계시 문제점

  • 전역 상태를 읽는 방법

    전역 상태는 여러 개의 값을 속성으로 가질 수 있지만, 전역 상태를 사용하는 모든 컴포넌트들이 전역 상태의 모든 속성 값들을 필요로 하지 않는 경우가 있다.
    따라서 이에 대한 리렌더링 최적화를 해주어야 한다.

  • 전역 상태 초기화 및 업데이트

    전역 상태는 여러 개의 값을 가질 수 있고, 일부는 중첩된 객체(?)일 수 있다. (이건 무슨 말인지 이해를 못했다;)
    개발자가 직접 변경하면 변경사항을 알아차릴 수 없기 때문에 제대로 작동하지 않을 수 있다.
    따라서 이를 감지하고 리렌더링할 수 있는 장치를 마련해주어야 한다.

데이터 중심 접근 방식과 컴포넌트 중심 접근 방식 사용하기

데이터 중심 접근 방식

모듈 상태가 리액트 외부 자바스크립트 메모리에 존재한다.
따라서 모듈 상태는 렌더링 시작 전, 마운트 해제 후에도 존재할 수 있다.
위 방식은 모듈 상태를 생성하고 모듈 상태를 리액트 컴포넌트에 연결하는 API를 제공한다.
모듈 상태는 보통 상태 변수에 접근하고 갱신하는 메서드를 가진 store 객체로 감싼다.

✅ 딱 보자마자 React Query가 생각났다.
서버로부터 받아온 데이터를 keyvalue 형태로 자바스크립트 메모리에 저장하고, 해당 데이터의 API와 데이터의 keyuseQuery에 파라미터로 넘겨 사용하기 때문이다.

컴포넌트 중심 접근 방식

컴포넌트 설계가 먼저 이루어진다.
어떤 경우에는 컴포넌트는 공유 정보에 접근해야 할 수도 있다.
따라서 props drilling이 발생하거나 상태 끌어올리기 등을 사용할 수 있다.
이때 위의 방법들이 적절하지 않다면 전역 상태를 도입할 수 있는 것이다.

✅ 데이터보다는 컴포넌트의 설계를 중요시 하는 방식이기 때문에, 컴포넌트를 구현해가며 해당 컴포넌트의 기능을 위해 필요한 상태를 추가하여 사용하는 방식으로 이해했다.
따라서 컴포넌트간 상호작용이 필요할 경우, 전역 상태를 통해 drilling을 줄일 수 있다고 생각한다.

두 접근 방식의 예외

꼭 위의 두 방식 중 하나를 채택해서 사용해야하는 것은 아니다.
두 방식을 함께 사용할 수도 있다.

✅ 서버에 저장된 데이터가 컴포넌트의 기능에서 필요한 상태로 사용되는 경우가 아닐까 생각된다.
만약 메시지 기능이 있을때, 새로운 메시지가 왔음을 알려주기 위해선 서버에서 이에 대한 플래그를 저장해두어야 한다.
그리고 이를 클라이언트에 내려주고 상태로 만들어 화면에 보여줘야 하기 때문이다.

모듈 상태는 대체로 싱글턴 패턴으로 구현되지만 하위 트리에 대해 여러 모듈 상태를 만들 수도 있다.

✅ 프로바이더를 통해 컴포넌트 트리중 특정 서브 트리에 한해 해당 트리에서 사용할 모듈 상태를 만들 수 있다라고 말하는 것 같다.

리렌더링 최적화

전역 상태에는 여러 속성으로 구성된 객체일 수 있다.
이때 하나의 속성이 변경되었을때, 전역 상태를 사용하고 있는 모든 컴포넌트가 리렌더링된다면 불필요한 렌더링이 발생하여 성능이 내려갈 것이다.
따라서 전역 상태중 특정 속성을 사용하고 있는 컴포넌트에 한해서 리렌더링될 수 있도록 최적화가 필요하다.

선택자 함수 사용

책에서 계속 설명하고 있는 내용중 하나이다.
전역 상태의 특정 속성을 선택하여 해당 컴포넌트에서 상태로 활용할 수 있도록 한다.
따라서 선택자 함수는 동일한 입력이 주어졌을때, 동일한 결과를 반환하는 것이 중요하다.
추가로 일부분뿐만 아니라 파생된 형태로 사용할 수도 있다.
만약 선택자 함수가 객체를 반환한다면 메모이제이션을 통해 동일한 객체를 반환하도록 해야 한다.

✅ 객체를 메모이제이션한다는 것이 어떤 말인지 이해가 잘 안간다.
클로저의 형태로 메모이제이션한다는 것인지, 아니면 전역 상태 자체가 메모이제이션된 상태나 마찬가지이지 해당 값에서 잘 추출해와야 한다는 것인지…. 흠..

속성 접근 감지

선택자 함수를 사용하지 않고, 속성에 대한 접근을 감지하고 자동으로 렌더링 최적화를 하는 방법인 상태 사용 추적이 있다고 한다.
이를 구현하기 위해선 속성 접근을 확인하기 위한 Proxy가 필요하다.
하지만 파생된 값을 활용할 때 접근만으로 리렌더링을 하기 때문에 의도치 않은 렌더링이 발생할 수 있다.

✅ 개념적으로 이런게 있다~ 정도를 말해주는 것 같다.
이전 장에서 보았던 useSyncExternalStore를 활용하거나, 이전 상태과 현재 상태을 직접 하나하나 비교하는 Proxy를 두어 구현할 수 있겠다는 생각이 든다.

아톰 사용

atom이란 리렌더링을 발생시키는데 사용되는 최소 단위의 상태이다.
따라서 atom을 통해 전역 상태를 세분화하여 구독하는 것이 가능하다.
파생 값을 활용하기도 편리하지만 파생 값에 활용한 atom의 의존성을 추적하여 해당 atom이 갱신될 때마다 다시 평가해야 한다.
atom과 파생 값의 정의는 명시적이지만, 의존성 추적은 자동으로 된다.

✅ atom을 사용한 라이브러리를 사용하면서 느낀 점은 atom으로 정의한 전역 상태들이 모두 독립적으로 존재하는 느낌을 받았다.
Map에 key와 value 형태로 전역 상태가 저장된 것처럼 말이다.
atom을 직접 정의하고 해당 atom을 사용하기 위해서는 어떤 atom을 사용할지 정의한 atom을 명시적으로 기입하여 변화에 대해 자동으로 리렌더링이 발생하기 때문에 정의는 명시적이지만 의존성 추적은 자동으로 된다라고 말한 것 같다.

Recoil과 Jotai가 atom을 사용한 방식으로 구현되어있다고 알고 있다.
Jotai는 제대로 아직 사용해본 경험이 없지만, Recoil에서는 Selector를 통해 파생 값까지 정의하여 편하게 활용할 수 있도록 기능을 제공한다.

이번에 나온 React Compiler에서는 리렌더링 최적화가 어느정도 자동으로 수행된다고 글을 봤던 것 같다.
따라서 useCallback, useMemo, memo의 사용을 줄일 수 있다고 말이다.
그리고 함께 봤던 내용들이 React Forget의 내용인데 조금더 살펴봐야할 것 같다.
이것들이 사실이라면 Context API와 전역 상태 라이브러리를 좀더 적극적으로 활용할 수 있게 되지 않을까 싶다.

도은

전역 상태 관리 문제 해결하기

전역 상태를 설계할 때 2가지 문제점

  1. 전역 상태를 읽는 방법
  • 전역 상태는 여러 값을 가질 수 있고
  • 전역 상태를 사용하는 컴포넌트는 전역 상태의 일부만 필요할 수 있다.
  • 전역 상태가 바뀌면 리렌더링이 일어나는데,
    • 일부만 사용하고 있는 컴포넌트고 뭐고 다 리렌더링이 발생
    • 바람직 X
  1. 전역 상태에 값을 넣거나 갱신하는 방법
  • 전역 상태는 충접된 객체일 수 있다.

  • 이럴 때 전역 변수를 가지고 개발자가 직접 값을 변경하는 것은 좋은 방법이 아닐 수 있다.

    globalVariable.b.d = 9;

    이렇게 변경할 경우, 이를 감지하고 컴포넌트를 리렌더링할 수 없다.

데이터 중심 접근 방식과 컴포넌트 중심 접근 방식 사용하기

  • 전역 상태는 **데이터 중심**과 **컴포넌트 중심**이라는 2가지 유형으로 구분

1. 데이터 중심 접근 방식 이해하기

  • 모듈 상태가 리액트 외부의 자바스크립트 메모리에 위치
  • 모듈 상태는 리액트가 렌더링을 시작하기 전이나 모든 리액트 컴포넌트가 마운트 해제된 후에도 존재 가능
  • 모듈 상태 생성 → 모듈 상태를 리액트 컴포넌트에 연결

2. 컴포넌트 중심 방식 이해하기

  • 데이터 모델이 컴포넌트에 강한 의존성을 가지고 있다.
  • 전역 상태는 서로 다른 컴포넌트 하위 트리에 존재할 수 있다.

리렌더링 최적화

핵심은 컴포넌트에서 state의 어느 부분이 사용될지 지정하는 것

  • state의 일부분을 지정하는 접근 방식
    • 선택자 함수 사용
    • 속성 접근 감지
    • 아톰 사용

1. 선택자 함수 사용

  • 선택자 함수는 상태를 받아 상태의 일부를 반환
const Component = () => {
  const value = useSelector((state) => state.b.c);
  return <div>{value}</div>;
};

📌 선택자와 메모이제이션에 대한 주요 사항

  • 선택자 함수가 반환하는 값이 숫자와 같은 원시 값이면 문제 X
  • 선택자 함수가 객체를 반환하는 경우 메모이제이션을 사용하여
  • 동일한 객체를 반환하도록 해야 한다.

2. 속성 접근 감지

  • 속성 접근을 감지하고, 감지한 정보를 렌더링 최적화 → 상태 사용 추적(state usage tracking)

3. 아톰 사용

  • 아톰: 리렌더링을 발생시키는 데 사용되는 최소 상태 단위

  • 전체 전역 상태를 구독해서 리렌더링을 피하는 대신 아톰을 사용하면 좀 더 세분화해서 구독하는 것이 가능

    const globalState = {
      a: atom(1),
      b: atom(2),
      c: atom(3),
    };
     
    const Component = () => {
      const value = useAtom(globalState.a);
    };