5장 리액트와 상태관리 라이브러리

5.1 상태관리는 왜 필요한가? 5.2 리액트 훅으로 시작하는 상태관리
강석우's avatar
Mar 13, 2024
5장 리액트와 상태관리 라이브러리

5.1.1 리액트 상태관리의 역사

Flux 패턴의 등장

기존에 사용되고 있던 MVC패턴은 양방향 데이터 바인딩이 가능하여 복잡도가 증가한다고 페이스북 팀이 보았다.
이에 양방향이 아닌 단방향으로 데이터 흐름을 변경하는 것을 제안하였고 이것이 Flux패턴의 시작이다.

아래는 MVC 패턴의 개념도다.

아래는 Flux패턴의 개념도다.

  • action : 작업을 처리할 액션과 액션 발생 시 포함시킬 데이터를 의미한다. 액션 타입과 데이터를 각각 정의해 이를 디스패처로 보낸다.

  • dispatcher : 액션을 스토어로 보내는 역할을 한다.

  • store : 실제 상태에 따른 값과 상태를 변경할 수 있는 메서드를 가지고 있다. 액션 타입에 따라 어떻게 변경할지가 정의 되어있다.

  • view : 컴포넌트에서 해당하는 부분으로 스토어에서 만들어진 데이터를 가져와 화면을 렌더링한다.

Elm 아키텍처

  • model : 애플리케이션의 상태

  • view : 모델을 표현하는 html

  • update : 모델을 수정하는 방식으로 Increment, Decrement를 선언해 모델을 수정한다.

리덕스

상태 주입이 아닌 전역상태관리를 위해 만들어진 라이브러리

액션의 타입 선언 => 액션을 수행할 creator 생성 => dispatcher, selector 생성 등 보일러 플레이트가 굉장히 많다.

Context API 와 useContext

끝없는 prop 내려주기라 불리는 방식을 없애기 위해 만들어진 리액트의 훅이다.
하지만 상태관리가 아닌 상태주입으로 상태에 대해 변화를 시킬 수는 없고 prop으로 상태를 여러단계 건너뛰고 주는 역할을 한다.

ReactQuery

외부에서 데이터를 불러오는 fetch를 관리하는 데 특화된 라이브러리이다. 하지만 api 호출에 대한 상태를 관리하고 있기에 http 요청에 특화된 상태 관리 라이브러리이다.

전역 상태관리 툴을 대체할 수 없지 않냐 라고 묻는다면 맞다.
그래서 보통 ReactQuery 와 전역상태관리 라이브러리를 같이 쓰게 되는데 사실 ReactQuery는 데이터로 가져온 값을 캐시해놓고 dependency가 변경되지 않으면 재요청 없이 받아온 값을 보여준다. 따라서 웬만하면 전역변수 없이 코딩이 가능하다.

React Query 에 대해서는 다음에 새로 글을 작성해보도록 하자.

Recoil, Zustand, Jotai, Valtio

훅을 사용해 상태를 가져오거나 관리할 수 있는 다양한 라이브러리이다.

// Recoil
const counter = atom({ key: 'count', default: 0 })
const todoList = useRecoilValue(counter)

// Jotai
const countAtom = atom(0)
const [count, setCount] = useAtom(countAtom)

// Zustand
const useCounterStore = create((set) => ({
 count: 0,
 increase: () => set((state) => ({ count: state.count + 1 })),
}))
const count = useCounterStore((state) => state.count)

// Valtio
const state = proxy({ count: 0 })
const snap = useSnapshot(state)
state.count++

위는 예시코드이며 지금 회사에서는 Recoil 을 사용하고 있다.
하지만 Recoil은 말이 많이 나오고 있으니 ( 개발사에서 더 이상의 업데이트에 대해 회의적으로 보고있는 것 같다. ) 속히 다른 상태관리 툴로 넘어가도록 하자.

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

5.2.1 useState와 useReducer를 사용한 상태관리

useState 와 useReducer는 우리가 가장 흔하게 사용하고 있는 상태관리 방식이다.
하지만 useState 또는 useReducer를 사용해 정의한 사용자 훅의 경우 컴포넌트를 선언할 때 마다 값을 초기화 해주기 때문에 컴포넌트별로 상태의 파편화가 일어날 수 밖에 없다.
해결책은 무엇이 있을까??

컴포넌트의 상위 컴포넌트로 상태를 옮겨주면 된다.
상위 컴포넌트에서 상태를 관리하고 하위 컴포넌트에 prop으로 상태를 drilling 해줄 경우 각각의 컴포넌트는 최상단에 선언된 상태를 참조하게 되기 때문에 파편화가 일어나지 않는다.

5.2.2 useState의 상태를 바깥으로 분리하기

  • global 또는 window 일 필요는 없지만 컴포넌트 외부에 상태를 두고 여러 컴포넌트가 사용할 수 있어야 한다.

  • 상태의 변화를 알아챌 수 있어야하고 상태의 변화마다 리렌더링이 일어나 컴포넌트를 최신 값으로 렌더링 시켜주어야한다.

  • 상태가 원시값이 아닌 객체인 경우에 그 객체에 내가 감지하지 않는 값이 변한다 하더라도 리렌더링이 발생해서는 안 된다. 예를 들어, {a: 1, b: 2}라는 상태가 있으며 어느 컴포넌트에서 a를 2로 업데이트했다고 가정해 보자. 이러한 객체 값의 변화가 단순히 b의 값을 참조하는 컴포넌트에서는 리렌더링을 일으켜서는 안 된다는 뜻이다

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

예제를 보며 이해하도록 하자

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

Recoil과 Jotai 는 Context와 Provider 그리고 훅을 기반으로 가능한 작은 상태를 효율적으로 관리하는 데 초첨을 맞춘다.

Zustand는 리덕스와 비슷하게 하나의 큰 스토어를 기반으로 상태를 관리하는 라이브러리이다.

Recoil

RecoilRoot

recoil을 사용하려는 컴포넌트의 최상단에 위치해 전체를 감싸주어야 한다.
recoilRoot에 recoil 에서 생성되는 상태값을 저장하기 위한 스토어를 생성하기 때문이다.

  • Recoil의 상태값은 RecoilRoot로 생성된 Context의 스토어에 저장된다.

  • 스토어의 상태값에 접근할 수 있는 함수들이 있으며, 이 함수를 활용해 상태값에 접근하거나 상태값을 변경할 수 있다.

  • 값의 변경이 발생하면 이를 참조하고 있는 하위 컴포넌트에 모두 알린다.

atom

recoil의 최소 상태 단위다.
atom은 key 값을 필수로 가지며 다른 atom과 구별하는 식별자가 되는 필수 값이다.
key는 애플리케이션에서 내부에서 유일한 값이어야 하기 때문에 주의를 기울여야 한다.
default는 atom의 초깃값을 의미한다.

type Statement = {
 name: string
 amount: number
}

const InitialStatements: Array<Statement> = [
 { name: '과자', amount: -500 },
 { name: '용돈', amount: 10000 },
 { name: '네이버페이충전', amount: -5000 }
]

// Atom 선언
const statementsAtom = atom<Array<Statement>>({
 key: 'statements',
 default: InitialStatements,
})

useRecoilValue

atom의 값을 읽어오는 훅이다. 아래와 같이 atom의 값을 가져올 수 있다.

const statements = useRecoilValue(statementsAtom)

useRecoilState

useState와 유사하게 값을 가져오고, 이 값을 변경할 수 있는 훅이다.

const [, setCount] = useRecoilState(counterState)

Jotai

atom

최소 단위의 상태를 의미한다. atom을 생성할 때 별도의 key를 넘겨주지 않아도 된다.

const counterAtom = atom(0)

console.log(counterAtom)
// {
// init: 0,
// read: (get) => get(config),
// write: (get, set, update) =>
// set(config, typeof update === 'function' ? update(get(config)) : update)
// }

useAtomValue

useAtom

useState와 동일한 형태의 배열을 반환한다.
첫 번째로 atom의 현재 값을 나타내는 useAtomValue의 결과를 반환,
두 번째로 useSetAtom 훅을 반환한다.

사용법

import { atom, useAtom, useAtomValue } from "jotai";
const counterState = atom(0);

function Counter() {
  const [, setCount] = useAtom(counterState);
  function handleButtonClick() {
    setCount((count) => count + 1);
  }
  return (
    <>
      <button onClick={handleButtonClick}>+</button>
    </>
  );
}

const isBiggerThan10 = atom((get) => get(counterState) > 10);
function Count() {
  const count = useAtomValue(counterState);
  const biggerThan10 = useAtomValue(isBiggerThan10);
  return (
    <>
      <h3>{count}</h3>
      <p>count is bigger than 10: {JSON.stringify(biggerThan10)}</p>
    </>
  );
}

export default function App() {
  return (
    <>
      <Counter />
      <Count />
    </>
  );
}

Zustand

리덕스에서 영감을 받아 만들어진 라이브러리이다.
하나의 스토어를 중앙 집중형으로 활용해 이 스토어 내부에서 상태를 관리한다.
 

Share article
RSSPowered by inblog