[모던 리액트 Deep Dive] 05장 리액트와 상태 관리 라이브러리

H Kim·2024년 3월 24일
0

기술 책 읽기

목록 보기
18/20
post-thumbnail
post-custom-banner

5.1 상태 관리는 왜 필요한가?

  • 웹 애플리케이션에서 상태로 분류될 수 있는 것들
    • UI: 기본적으로 웹 애플리케이션에서 상태라 함은 상호 작용이 가능한 모든 요소의 현재 값을 의미한다. 다크/라이트 모드, 라디오를 비롯한 각종 input, 알림창의 노출 여부 등 많은 종류의 상태가 존재한다.
    • URI: 브라우저에서 관리되고 있는 상태값이다.
    • 폼(form): 로딩중인지(loading), 제출됐는지(submit), 접근이 불가능한지(disabled), 값이 유효한지(validation) 등 모두가 상태로 관리된다.
    • 서버에서 가져온 값: 클라이언트에서 서버로 요청을 통해 가져온 값도 상태로 볼 수 있다. 대표적으로 API 요청이 있다.


5.1.1 리액트 상태 관리의 역사

  • Flux 패턴의 등장
    • 웹 애플리케이션이 비대해지고 상태(데이터)도 많아짐에 따라 어디서 어떤 일이 일어나서 이 상태가 변했는지 등을 추적하고 이해하기가 매우 어려운 상황을, 페이스북 팀은 이 원인을 양방향 데이터 바인딩으로 봤다. 뷰(HTML)가 모델(자바스크립트)을 변경할 수 있으며, 반대의 경우 모델도 뷰를 변경할 수 있다. 이는 코드를 작성하는 입장에서는 간단할 수 있지만 코드의 양이 많아지고 변경 시나리오가 복잡해질수록 관리가 어려워진다. 페이스북 팀은 양방향이 아닌 단방향으로 데이터 흐름을 변경하는 것을 제안하는데 이것이 Flux 패턴의 시작이다.
    • 액션(action): 어떠한 작업을 처리할 액션과 그 액션 발생 시 함께 포함시킬 데이터를 의미한다. 액션 타입과 데이터를 각각 정의해 이를 디스패처로 보낸다.
    • 디스패처(dispatcher): 액션을 스토어에 보내는 역할을 한다. 콜백 함수 형태로 앞서 액션이 정의한 타입과 데이터를 모두 스토어에 보낸다.
    • 스토어(store): 여기에서 실제 상태에 따른 값과 상태를 변경할 수 있는 메서드를 가지고 있다. 액션의 타입에 따라 어떻게 이를 변경할지가 정의돼 있다.
    • 뷰(view): 리액트의 컴포넌트에 해당하는 부분으로, 스토어에서 만들어진 데이터를 가져와 화면을 렌더링하는 역할을 한다. 또한 뷰에서도 사용자의 입력이나 행위에 따라 상태를 업데이트하고자 할 수 있을 것이다. 이 경우에는 다음 그림처럼 뷰에서 액션을 호출하는 구조로 구성된다.

  • 사용자의 입력에 따라 데이터를 갱신하고 화면을 어떻게 업데이트해야 하는지도 코드로 작성해야 하므로 코드의 양이 많아지고 개발자도 수고로워진다는 단점이 존재한다.

  • 리덕스
    - 피피피
    • 최초에는 Flux 구조를 구현하기 위해 만들어진 라이브러리 중 하나.
    • Elm 아키텍쳐(웹페이지를 선언적으로 작성하기 위한 언어)를 도입했다는 것이 차이점이다.
    • 모델(model): 애플리케이션의 상태를 의미한다.
    • 뷰(view): 모델을 표현하는 HTML을 말한다.
    • 업데이트(update): 모델을 수정하는 방식을 말한다.
    • 하나의 상태 객체를 스토어에 저장해 두고, 이 객체를 업데이트하는 작업을 디스패치해 업데이트를 수행한다. 이러한 작업은 reducer 함수로 발생시킬 수 있는데, 이 함수의 실행은 웹 애플리케이션 상태에 대한 완전히 새로운 복사본을 반환한 다음, 애플리케이션에 이 새롭게 만들어진 상태를 전파하게 된다.
    • 하나의 글로벌 상태 객체를 통해 이 상태를 하위 컴포넌트에 전파할 수 있기 때문에 props를 깊이 전파해야 하는 이른바 props 내려주기 문제를 해결할 수 있었고, 스토어가 필요한 컴포넌트라면 단지 connect만 쓰면 스토어에 바로 접근할 수 있다.

  • Context API와 useContext
    • 리액트 팀은 리액트 16.3에서 전역 상태를 하위 컴포넌트에 주입할 수 있는 새로운 Context API를 출시했다. props로 상태를 넘겨주지 않더라도 Context API를 사용하면 원하는 곳에서 Context Provider가 주입하는 상태를 사용할 수 있게 됐다.

  • Hooks의 탄생, React Query & SWR
    • 훅의 탄생으로 React Query와 SWR이 등장하였다.
    • 두 라이브러리는 모두 외부에서 데이터를 불러오는 fetch를 관리하는 데 특화된 라이브러리지만, API 호출에 대한 상태를 관리하고 있기 때문에 HTTP 요청에 특화된 상태 관리 라이브러리라 볼 수 있다.
    • useSWR은 첫 번째 인수로 조회할 API 주소를, 두 번째 인수로 조회에 사용되는 fetch를 넘겨준다. 첫 번째 인수인 API 주소는 키로도 사용되며, 이후에 다른 곳에서 동일한 키로 호출하면 재조회하는 것이 아니라 useSWR이 관리하고 있는 캐시의 값을 활용한다.

  • Recoil, Zustand, Jotai, Valtio 등
    • 요즘 새롭게 떠오르고 있는 많은 상태 관리 라이브러리는 기존의 리덕스와는 달리 훅을 활용해 작은 크기의 상태를 효율적으로 관리한다는 것이다.


5.2 리액트 훅으로 시작하는 상태 관리

5.2.1 가장 기본적인 방법: useState와 useReducer

  • useState와 useReducer가 상태 관리의 모든 필요성과 문제를 해결해 주지는 않는다. useState와 useReducer를 기반으로 하는 사용자 지정 훅의 한계는 명확하다 훅을 사용할 때마다 컴포넌트별로 초기화되므로 컴포넌트에 따라 서로 다른 상태를 가질 수밖에 없다. 예제의 경우 counter는 useCounter가 선언될 때마다 새롭게 초기화되어, 결론적으로 컴포넌트별로 상태의 파편화를 만들어 버린다. 이렇게 기본적인 useState를 기반으로 한 상태를 지역상태(local state)라고 하며, 이 지역 상태는 해당 컴포넌트 내에서만 유효하다는 한계가 있다.

5.2.2 지역 상태의 한계를 벗어나보자: useState의 상태를 바깥으로 분리하기

  • 업테이트되는 값을 가져오려면 상태를 업데이트하는 것뿐만 아니라 상태가 업데이트됐을 때 이를 컴포넌트에 반영시키기 위한 리렌더링이 필요하며, 2.4절 '렌더링은 어떻게 일어나는가?'에서 살펴본 것처럼 함수형 컴포넌트에서 리렌더링을 하려면 다음과 같은 작업 중 하나가 일어나야 한다.
    • useState, useReducer의 반환값 중 두 번째 인수가 어떻게든 호출된다. 설령 그것이 컴포넌트 렌더링과 관계없는 직접적인 상태를 관리하지 않아도 상관없다. 어떠한 방식으로든 두 번째 인수가 호출되면 리액트는 다시 컴포넌트를 렌더링한다.
    • 부모 함수(부모 컴포넌트)가 리렌더링되거나 해당 함수(함수형 컴포넌트)가 다시 실행돼야 한다.

  • useState로 컴포넌트의 리렌더링을 실행해 최신값을 가져오는 방법은 어디까지나 해당 컴포넌트 자체에서만 유효한 전략이다.

  • 함수 외부에서 상태를 참조하고 이를 통해 렌더링까지 자연스럽게 일어나려면 다음과 같은 조건을 만족해야 한다는 결론에 도달한다.

      1. 꼭 window나 global에 있어야 할 필요는 없지만 컴포넌트 외부 어딘가에 상태를 두고 여러 컴포넌트가 같이 쓸 수 있어야 한다.
      1. 이 외부에 있는 상태를 사용하는 컴포넌트는 상태의 변화를 알아챌 수 있어야 하고 상태가 변화될 때마다 리렌더링이 일어나서 컴포넌트를 최신 상태값 기준으로 렌더링해야 한다. 이 상태 감지는 상태를 변경시키는 컴포넌트뿐만 아니라 . 이상태를 참조하는 모든 컴포넌트에서 동일하게 작동해야 한다.
      1. 상태가 원시값이 아닌 객체인 경우에 그 객체에 내가 감지하지 않는 값이 변한다 하더라도 리렌더링이 발생해서는 안 된다. 예를 들어, {a: 1, b: 2}라는 상태가 있으며 어느 컴포넌트에서 a를 2로 업데이트했다고 가정해 보자. 이러한 객체 값의 변화가 단순히 b의 값을 참조하는 컴포넌트에서는 리렌더링을 일으켜서는 안 된다는 뜻이다.

5.2.3 useState와 Context를 동시에 사용해 보기

  • 이 훅과 스토어를 사용하는 구조는 반드시 하나의 스토어만 가지게 된다는 것이다. 하나의 스토어를 가지면 이 스토어는 마치 전역 변수처럼 작동하게 되어 동일한 형태의 여러 개의 스토어를 가질 수 없게 된다.

  • 상태 관리 라이브러리가 작동하는 방식

    • useState, useReducer가 가지고 있는 한계, 컴포넌트 내부에서만 사용할 수 있는 지역 상태라는 점을 극복하기 위해 외부 어딘가에 상태를 둔다. 이는 컴포넌트의 최상단 내지는 상태가 필요한 부모가 될 수도 있고, 혹은 격리된 자바스크립트 스코프 어딘가일 수도 있다.
    • 이 외부의 상태 변경을 각자의 방식으로 감지해 컴포넌트의 렌더링을 일으킨다.

5.2.4 상태 관리 라이브러리 Recoil, Jotai, Zustand 살펴보기

  • Rocil과 Jotai는 Context와 Provider, 그리고 훅을 기반으로 가능한 작은 상태를 효율적으로 관리하는 데 초점을 맞추고 있다. 그리고 Zustand는 리덕스와 비슷하게 하나의 큰 스토어를 기반으로 상태를 관리하는 라이브러리다. Recoil, Jotai와는 다르게 이 하나의 큰 스토어는 Context가 아니라 스토어가 가지는 클로저를 기반으로 생성되며, 이 스토어의 상태가 변경되면 이 상태를 구독하고 있는 컴포넌트에 전파해 리렌더링을 알리는 방식이다.


  • 페이스북이 만든 상태관리 라이브러리 Recoil
    • 최소 상태 개념인 Atom을 처음 리액트 생태계에서 선보이기도 했다.
    • Recoil의 핵심 API인 RecoilRoot, atom, useRecoilValue, useRecoilState가 있다.
  • RecoilRoot
    • Recoil을 사용하기 위해서는 RecoilRoot를 애플리케이션의 최상단에 선언해 둬야 한다.
    • RecoilRoot에서 Recoil에서 생성되는 상태값을 저장하기 위한 스토어를 생성하는 것을 확인할 수 있다.
    • useStoreRef로 ancestorStoreRef의 존재를 확인하는데, 이는 Recoil에서 생성되는 atom과 같은 상태값을 저장하는 스토어를 의미한다. 그리고 이 useStoreRef가 가리키는 것은 다름 아닌 AppContext가 가지고 있는 스토어다.
    • Recoil의 상태값은 RecoilRoot로 생성된 Context의 스토어에 저장된다.
    • 스토어의 상태값에 접근할 수 있는 함수들이 있으며, 이 함수를 활용해 상태값에 접근하거나 상태값을 변경할 수 있다.
    • 값의 변경이 발생하면 이를 참조하고 있는 하위 컴포넌트에 모드 알린다.
  • atom
    • 상태를 나타내는 Recoil의 최소 상태 단위다.
    • key 값을 필수로 가지며, 이 키는 다른 atom과 구별하는 식별자가 되는 필수 값이다. 이 키는 애플리케이션 내부에서 유일한 값이어야 하기 때문에 atom과 selector를 만들 때 반드시 주의를 기울여야 한다.
  • useRecoilValue
    • atom의 값을 읽어오는 훅이다.
  • useRecoilState
    • useRecoilValue는 단순히 atom의 값을 가져오기 위한 훅이었다면 useRecoilState는 좀 더 useState와 유사하게 값을 가져오고, 또 이 값을 변경할 수도 있는 훅이다.

  • selector는 한 개 이상의 atom 값을 바탕으로 새로운 값을 조립할 수 있는 API로, useStoreSelector와 유사한 역할을 수행하는 것을 확인할 수 있다.
  • selector를 필두로 다양한 비동기 작업을 지원하는 API를 제공하고 있기 때문에 리덕스와 달리 redux-saga나 redux-thunk 등 추가적인 미들웨어를 사용하지 않더라도 비동기 작업을 수월하게 처리할 수 있다.

  • Recoil에서 영감을 받은, 그러나 조금 더 유연한 Jotai
    • Recoil의 atom 모델에 영감을 받아 만들어진 상태 관리 라이브러리다. Jotai는 상향식(bottom-up) 접근법을 취하고 있다고 나와 있는데 이는 리덕스와 같이 하나의 큰 상태를 애플리케이션에 내려주는 방식이 아니라, 작은 단위의 상태를 위로 전파할 수 있는 구조를 취하고 있음을 의미한다 또한 앞서 언급했던 리액트 Context의 문제점인 불필요한 리렌더링이 일어난다는 문제를 해결하고자 설계돼 있으며, 추가적으로 개발자들이 메모이제이션이나 최적화를 거치지 않아도 리렌더링이 발생되지 않도록 설계돼 있다.
  • atom
    • Recoil과 마찬가지로 최소 단위의 상태를 의미한다. atom 하나만으로도 상태를 만들 수도, 또 이에 파생된 상태를 만들 수도 있다.
  • useAtomValue
    • store에 atom 객체 그 자체를 키로 활용해 값을 저장한다.
  • useAtom
    • 첫 번째로는 atom의 현재 값을 나타내는 useAtomValue 훅의 결과를 반환하며, 두 번째로는 useSetAtom 훅을 반환하는데, 이 훅은 atom을 수정할 수 있는 기능을 제공한다.

  • Recoil의 atom에서는 각 상태값이 모두 별도의 키를 필요로 하기 때문에 이 키를 별도로 관리해야 하는데, Jotai는 이러한 부분을 추상화해 사용자가 키를 관리할 필요가 없다. Jotai가 별도의 문자열 키가 없이도 각 값들을 관리할 수 있는 것은 객체의 참조를 통해 값을 관리하기 때문이다. 객체의 참조를 WeakMap에 보관해 해당 객체 자체가 변경되지 않는 한 별도의 키가 없이도 객체의 참조를 통해 값을 관리할 수 있다.

  • 작고 빠르며 확장에도 유연한 Zustand
    • 리덕스에 영감을 받아 만들어졌다. 하나의 하나의 스토어를 중앙 집중형으로 활용해 이 스토어 내부에서 상태를 관리하고 있다.
    • createStore로 스토어를 만들 때 set이라는 인수를 활용해 생성할 수 있다는 것이다. 이는 앞선 Zustand의 createStore 예제 코드에서 살펴볼 수 있는 것처럼 state를 생성할 때 setState, getState, api를 인수로 넘겨줬기 때문에 가능하다. set을 통해 현재 스토어의 값을 재정의할 수도 있고, 두번째 인수로 get을 추가해 현재 스토어의 값을 받아올 수도 있다.
    • Jotai와 마찬가지로 타입스크립트 기반으로 작성돼 있기 때문에 별도로 @types를 설치하거나 임의로 작성된 d.ts에 대한 우려 없이 타입스크립트를 자연스럽게 쓸 수 있다.
post-custom-banner

0개의 댓글