상태 관리는 왜 필요한가?

상태

어떠한 의미를 지닌 값

에플리케이션의 시나리오에 따라 변경될 수 있는 값

  • UI
  • URL
  • 서버에서 가져온 값...

다양한 기능에 띠라 관리해야 할 상태도 점차 증가하고 있습니다.

상태관리의 역사

Flux 패턴의 등장

text

기존 MVC 패턴은 상태(데이터)가 많아짐에 따라서 복잡도가 증가하게 됩니다.

이 문제의 원인은 바로 양방향 이라고 생각했습니다.

alt text

단방향으로 데이터 흐름을 변경한 것이 Flux 패턴의 시작입니다.

  • action: 어떠한 작업을 처리할 액션과 그 액션 발생 시 함께 포함시킬 데이터를 의미합니다. 액션 타입과 데이터를 각각 정의해 이를 디스패처로 보냅니다.
  • dispatcher: 액션을 스토어에 보내는 역할을 합니다. 콜백 함수 형태로 앞서 액션이 정의한 타입과 데이터를 모두 스토어에 보냅니다.
  • store: 여기에서 실제 상태에 따른 값과 상태를 변경할 수 있는 메서드를 가지고 있습니다. 액션의 타입에 따라 어떻게 이를 변경할지가 정의돼 있습니다.
  • view: 리액트의 컴포넌트에 해당하는 부분으로, 스토어에서 만들어진 데이터를 가져와 화면을 렌더링하는 역할으 합니다. 또한 뷰에서도 사용자의 입력이나 행위에 따라 상태를 업데이트하고자 할 수 있을 것 입니다. 이 경우에는 다음 그림처럼 뷰에서 액션을 호출하는 구조로 구성됩니다.

alt text

단방향 데이터

사용자의 입력에 따라 데이터를 갱신하고 화면을 어떻게 업데이트해야 하는지도 코드로 작성해야합니다.

코드의 양이 많아지고 개발자도 수고로워집니다.

데이터의 흐름은 모두 액션이라는 한 방향(단방향)으로 줄어들므로 데이터의 흐름을 추적하기가 용이합니다.

리덕스의 등장

리덕스는 Flux 구조에 Elm 아키텍처를 도입했습니다.

Elm 아키텍처

웹 페이지를 선언적으로 작성하기 위한 언어

Elm으로 HTML을 작성한 예시

module Main exposing(..)

import Browser
import Html
import Html.Events exposing (onClick)

---Main

main=
  Browser.sandbox = {init=init, update=update, view=view}

---MODEL

type alias Model = Int

init: Model
init = 0

--UPDATE

type Msg
=Increment
| Decrement

update: Msg -> Model -> Model
update msg model = 
case msg of
Increment -> 
model + 1

Decrement ->
model - 1

--VIEW

view: Model -> Html Msg
view model =
div []
[ button [OnClick Decrement][text "-"]
  , div [] [text(String.fromInt model)]
  , button [onClick Increment ][text "+"]
]

<div>
<button>-</button>
<div>2</div>
<button>-</button>
</div>

여기서 주목할 것은 model,update,view 입니다.

  • 모델: 애플리케이션의 상태를 의미합니다. 여기서는 Model을 의미하며, 초깃값으로는 0이 주어졌습니다.
  • 뷰: 모델을 표현하는 HTML을 말합니다. 여기서는 Model을 인수로 받아서 HTML을 표현합니다.
  • 업데이트: 모델을 수정하는 방식을 말합니다. Increment, Decrement 를 선언해 가각의 방식이 어떻게 모델을 수정하는지 나타냈습니다.

Elm은 Flux와 마찬가지로 데이터 흐름을 세 가지로 분류하고, 이를 강제해 웹 애플리케이션의 상태를 안정적으로 관리하고자 노력했습니다.

리덕스

하나의 상태 객체를 스토어에 저장해 두고, 이 객체를 업데이트하는 작업을 디스패치해 업데이트를 수행합니다.

이러한 작업을 reducer가 하게 되는데, 이 함수의 실행은 웹 애플리케이션 상태에 대한 완전히 새로운 복사본을 반환한 다음, 애플리케이션에 이 새롭게 만들어진 상태를 전파하게 됩니다.

-> props drilling 문제를 해결할 수 있게 되었습니다!

그렇다고 무조건 좋은건 아님..

  • 보일러 플레이트 코드 많음
    • 근데 이건, tool-kit 나오면서 많이 완화되었다고 합니다!

Context API와 useContext

리액트 16.3 버전부터 전역 상태를 하위 컴포넌트에 주입할 수 있는 새로운 Context API를 출시했습니다.

16.3 버전 이전에도 context는 존재했습니다.

getChildContext() 제공

(코드는 책을 참조해주세요)

문제

  • 상위 컴포넌트가 렌더링되면 getChildCOntext도 호출됨과 동시에 shouldComponentUpdate가 항상 true를 반환해 불필요하게 렌더링이 일어납니다.
  • getChildContext를 이용하기 위해서 context를 인수로 받아야 했는데 이 때문에 컴포넌트와 결합도가 높아집니다.

Context API

type Counter = {
  count : number
}
 
const CounterContext = createContext<Counter | undefined>(undefined)
 
class CounterComponent extends Component {
  render() {
    return (
      <CounterContext.Consumer>
        {(state)=><p>{state?.count}</p>}
      </CounterContext.Consumer>
    )
  }
}
 
class DummyParent extends Component {
  return() {
    return (
      <>
        <CounterComponent />
      </>
    )
  }
}
 
export default class MyApp extends Component<{},Counter> {
  state = { count: 0 }
 
  componentDidMount() {
    this.setState({count:1})
  }
 
  handleClick = () => {
    this.setState((state)=>({count: state.count +1}))
  }
 
  render(){
    return(
      <CounterContext.Provider value={this.state}>
        <button onClick ={handleClick}>+</button>
        <DummyParent />
      </CounterContext.Provider>
    )
  }
}
  • MyApp에 상태가 선언되어 있음(Context로 주입)
  • Provider로 주입된 상태는 자식의 자식인 CounterCOmponent에서 사용하고 있습니다.

훅의 탄생, React Query와 SWR

리액트는 16.8 버전에서 함수 컴포넌트에 사용할 수 있는 다양한 훅 API를 추가했습니다.

이러한 훅의 등장으로 React QuerySWR dl emdwkdgkrp ehlqslek.

두 라이브러리 모두 외부에서 데이터를 불러오는 fetch를 관리한데 특화된 라이브러리 이지만, API 호출에 대한 상태를 관리하고 있기 때문에 HTTP 요청에 특화된 상태 관리 라이브러리라고 볼 수 있습니다.

SWR

import React from 'react'
import useSWR from 'swr'
 
const fetcher = (url) => fetch(url).then((res)=>res.json())
 
export default function App() {
  const {data,error} = useSWR(
    'url',
    fetcher,
  )
 
  if(error) return '에러 발생'
  if(!data) return '로딩중...'
 
  return (
    <div>
      <p>{JSON.stringify(data)}</p>
    </div>
  )
}

useSWR의 첫 번째 인수로 조회할 API 주소, 두 번째 인수로 조회에 사용되는 fetchㅡㄹ ㄹ넘겨줍니다.

첫번째 인수인 API 주소는 키로도 사용되며, 다른 곳에서 동일한 키로 호출하면 재조회되는게 아니라 캐시의 값을 활용합니다.

Recoil, Zustand, Jotai, Valtio..에 이르기까지

훅을 활용해 작은 크기의 상태를 효율적으로 관리합니다.

쉽게 말해 react의 단방향 데이터 흐름 특성으로 인해 생기는 불편함을 해소시켜줍니다. useState를 사용하는 경우, 상태 전달이 번거로웠지만, atom을 이용하여 해당 상태 구조를 정의해주기만 하면, 그 상태를 어느 컴포넌트에서도 쉽게 접근할 수 있습니다.

alt text

상태관리라이브러리 비교

Redux vs Recoil vs Zustand vs Jotai

alt text

다운로드 수는 Redux가 오래된 만큼 가장 높습니다.

alt text

업데이트 측면에서는 recoil은 1년동안 업데이트가 되지않았기 때문에 사용하지 않는 편이 좋을 것 같습니다.(개인견해)

사이즈 측면에서는 Zustand가 가장 작습니다.

Jotai 상태를 일본말로 하면 Jotai라고 합니다.

  • 가벼움
  • atomic 한 상태 관리 방식
  • Typescript 기본 내장
  • recoil과 달리 키를 필요로 하지 않습니다.

초기값 atom으로 생성, useAtom으로 값을 받고 수정합니다.

import { atom, useAtom } from 'jotai';
const textAtom = atom('hello');
const Input = () => {
  const [text, setText] = useAtom(textAtom)
  const handleChange = (e) => setText(e.target.value)
  return (
    <input value={text} onChange={handleChange} />
  )
}

Zustand 독일어로 상태라는 뜻 입니다.

  • 유연성과 간결성
    • 아키텍처를 강조하지 않습니다.
  • 불필요한 렌더링 최소화
  • SSR과 호환성
  • 프로바이더가 없습니다.
  • 작은 번들 사이즈
import create from 'zustand';
 
// set 함수를 통해서만 상태를 변경할 수 있다
const useStore = create(set => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 })
}));

스토어를 만들 때는 create 함수를 이용하여 상태와 그 상태를 변경하는 액션을 정의합니다. 그러면 리액트 컴포넌트에서 사용할 수 있는 useStore 훅을 리턴합니다.

// 상태를 꺼낸다
function BearCounter() {
  const bears = useStore(state => state.bears);
  return <h1>{bears} around here ...</h1>;
}
 
// 상태를 변경하는 액션을 꺼낸다
function Controls() {
  const increasePopulation = useStore(state => state.increasePopulation);
  return <button onClick={increasePopulation}>one up</button>;
}

컴포넌트에서 useStore 훅을 사용할 때는 스토어에서 상태를 어떤 형태로 꺼내올지 결정하는 셀렉터 함수를 전달해 주어야 합니다. 만약 셀렉터 함수를 전달하지 않는다면 스토어 전체가 리턴됩니다.

Valtio proxy 기반의 상태 관리 라이브러리

참고블로그

[React] Recoil로 상태관리하기_atom, selector (opens in a new tab) Jotai는 조-타이 라고 읽습니다. (opens in a new tab) 왜 zustand 를 선택했는가 (opens in a new tab) React 상태 관리 라이브러리 Zustand의 코드를 파헤쳐보자 (opens in a new tab)