[React] Deep Dive 모던 리액트(15) 리액트와 상태관리 라이브러리 역사
FrontEnd/Deep Dive

[React] Deep Dive 모던 리액트(15) 리액트와 상태관리 라이브러리 역사

728x90

 

 

 

보통 상태관리라고 하면 많은 개발자들이 리덕스를 떠올릴 것이고 최근에 페이스북에서 만들어진 Recoil과 같은 라이브러리를 생각하곤 한다. 하지만 왜 상태관리가 필요하고 어떤 방식으로 전역상태 관리가 이루어지는지 간과하는 경우가 많다.

 

 

상태관리 ?

그렇다면 상태관리란 무엇일까? 흔히 웹 애플리케이션을 개발하는 경우 이야기 하는 상태는 어떠한 의미를 가진 값을 의미한다.

 

UI
기본적으로 웹 어플리케이션에서 상태는 상호작용이 가능한 모든 요소의 현재 값을 의미하므로 다크/라이트 모드 , input , 모달 등등 많은 종류의 상태가 존재한다.

URL
브라우저에서 관리되고 있는 상태값으로 URL에도 참고할 수 있는 상태가 존재할 수 있다.

FORM
로딩중,제출이되었는지 , 접근가능한지 등등 모든 것이 상태이다.ㅏ

API
클라이언트에서 서버로 요청을 보내 받은 값 또한 상태이다

 

 

단순히 서버에서 요청받은 값을 보여주기만 하던 시대와 달리 현재의 웹 애플리케이션은 다양한 상태를 효과적으로 관리하는 방법을 고민해야 한다. 물론 상태를 관리하는 일 자체는 크게 어려운일이 아니다. 단순하게 상태가 많아지면 그저 그에따른 작업량이 증가하는 것이라고도 볼 수 있다.

 

하지만 애플리케이션 전역에서 관리해야 하는 상태가 있다면 조금 골치가 아파진다. 상태를 어디다 둘 것인지 , 상태의 범위는 어떻게 제한할 수 있는지 , 상태변화는 어떻게 감지할 것인지 등등 고민할것이 많다.

또한 이러한 전역상태들이 일어날때마다 UI가 변경되면 애플리케이션이 찢어지는 현상 (tearing) 이 발생한다.

 

 

 

리액트 상태관리의 역사

 

애플리케이션 개발에 모든것을 제공하는 Angular와는 다르게 리액트는 단순한 라이브러리이다. 따라서 이 상태를 관리하는 방법도 개발자,시간의 흐름에 따라 다양한 방법이 존재한다.

 

 

Flux 패턴의 등장

순수 리액트에서 할 수 있는 상태관리 수단이라고 하면 Context API를 생각할 수 있다. 하지만 이 Context API는 엄밀하게는 상태관리보단 상태 주입을 도와주는 역할이다.

리덕스가 나오기 전까지 딱히 이름을 널린 상태관리 라이브러리는 없었다. 2014년 경 리액트 등장과 비슷한 시기에 Flux패턴과 함께 이를 기반으로 한 라이브러리인 Flux가 나왔다.

 

MVC패턴

 

 

기존 MVC패턴은 모델과 뷰가 많아질수록 복잡도가 굉장히 증가하게 되는데 페이스북 팀은 이러한 문제를 양방향 데이터 바인딩의 문제로 봤다. 따라서 페이스북팀은 양방향이 아닌 단방향 데이터 흐름을 제안했는데 이게 바로 Flux패턴의 시작이다.

 

 

 

Action
어떠한 작업을 처리할 액션과 액션 발생 시 포함시킬 데이터

Dispatcher
액션을 스토어에 보내는 역할을 한다.

Store
실제 상태에 따른 값과 상태를 변경할 수 있는 메서드를 가지고 있다.

View
리액트의 컴포넌트에 해당하는 부분이다. 스토어에서 만들어진 데이터를 가져와 화면을 렌더링한다.

 

 

 

해당 부분을 코드로 간단하게 봐보자.

 

import { useReducer } from "react";

type StoreState = {
  count: number;
};

type Action = { type: "add"; payload: number };

function reducer(prevState: StoreState, action: Action) {
  const { type: ActionType } = action;
  if (ActionType === "add") {
    return {
      count: prevState.count + action.payload,
    };
  }

  throw new Error(`Unexpected Action [${ActionType}]`);
}

export default function MyComponent() {
  const [state, dispatcher] = useReducer(reducer, { count: 0 });

  function handleClick() {
    dispatcher({ type: "add", payload: 1 });
  }

  return (
    <div>
      <h1>{state.count}</h1>
      <button onClick={handleClick}>+</button>
    </div>
  );
}

 

 

 

typeAction으로 인한 액션 정의 -> 스토어의 역할을 하는 useReducer,reducer -> dispatch로 액션실행 -> 뷰인 Mycomponent에서 보여줌

 

 

이러한 데이터의 흐름은 물론 불편함도 존재한다. 사용자 입력에 따라 화면을 업데이트 해야해서 코드의 양도 많아지고 개발자도 수고로워진다. 단 데이터의 흐름을 추적하기는 쉬워진다. 리액트 자체가 이런 단방향 데이터 바인딩을 기반으로 했어서 Flux패턴과 궁합이 잘 맞았다. 따라서 Flux패턴을 따르는 다양한 라이브러리들이 생겨났다.

 

Flux , alt , RefluxJS , NuclearJS , Fluxible , Fluxxor 등등..

 

 

리덕스의 등장

 

리덕스또한 최초에는 이 Flux구조를 구현하기 위해 만들어진 라이브러리였다. 조금 특별한 것은 여기에 Elm 아키텍처를 도입했다는 것이다.

 

Elm

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

module Main exposig(..)

import Browser
import Html exposing (Html,button,div,text)
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 mode =
	cas msg of
    	Increment ->
        	model + 1
        Decrement ->
        	model - 1

-- VIEW

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


코드자체는 조금 낯설게 보일 수 있다. 하지만 mode,update,view 가 있는점을 보자.

Elm은 Flux와 마찬가지로 데이터 프름을 세가지로 분류하고 이를 단방향으로 강제한다.

 

 

방금 설명한 Elm 아키텍처의 영향을 받아 작성되었으며 하나의 상태 객체를 스토어에 저장하고 해당 객체를 업데이트 하는 작업을 디스패치하여 업데이트를 수행한다. 이 작업을 reducer함수로 발생시킨다.

해당 함수의 실행은 웹 애플리케이션 상태에 대해 완전히 새로운 복사본을 반환하고 애플리케이션에 상태를 전파하게 된다.

 

 

이 리덕스의 등장은 리액트 생태계에 큰 영향을 미쳤다고 한다. 이제 글로벌 상태 객체가 되기때문에 prop내려주기 문제를 해결할 수 있고 dispatch가 필요없는 단순히 스토어가 필요한 컴포넌트도 connect만 쓰면 편하게 스토어에 접근할 수 있다.

 

물론 리덕스가 마냥 편한 것은 아니다. 단순히 상태를 바꾸는 작업을 진행해도 액션 타입설정 , 액션수행함수 , dispatch 등등 정의해야할 것이 많다. 그럼에도 리덕스는 표준처럼 자리잡았다!

 

 

Context API와 useContext

 

쭉 설명했듯이 상태를 어떻게 주입해야 하는지에 대한 고민은 계속 이어져왔다. 리액트팀은 리액트 16.3에서 전역 상태를 하위 컴포넌트에 주입할 수 있는 ContextAPI를 출시했다.

해당 버전 이전에도 context는 존재했으며 getChildContext()를 통해 다룰 수 있었다.

 

import React from "react"
class MyComponent extends React.Component {
    static childContextTypes = {
        name : PropTypes.string,
        age : PropTypes.number
    }

    getChildContext() {
        return {
            name : 'foo',
            age : 30
        }}
      
    render() {
        return <ChildComponent/>
    }
}

function ChildComponent() { 

  return (
    <div>
      <p>Name : {context.name}</p>
      <p>Age : {context.age</p>
    </div>
  )
}

ChildComponent.contextTypes = {
    name : PropTypes.string,
    age : PropTypes.number,
}

 

위 방식은 몇가지 문제가 있었따. 상위컴포넌트가 렌더링될때 shouldComponentUpdate가 항상 true가 되어 불필요하게 렌더링이 되고 context를 인수로 받아야 해 컴포넌트와 결합도가 높아지는 등의 단점이 존재했다.

 

이러한 문제를 해결하기 위해서 16.3버전 Context API가 사용되었다.

 

import React, { Component, createContext } from "react";

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 {
  render() {
    return (
      <>
        <CounterComponent />
      </>
    );
  }
}

export default class MyComponent 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={this.handleClick}>+</button>
        <DummyParent />
      </CounterContext.Provider>
    );
  }
}

 

 

부모 컴포넌트인 MyComponent에 상태가 선언되어 있지만 자식에서 state를 조회할 수 있다. 하지만 계속 말하듯이 Context API는 상태관리가 아닌 주입을 도와주는 라이브러리이니 사용할때 주의가 필요하다.

 

훅의 탄생 , React Query와 SWR

 

ContextAPI의 탄생하고 얼마 지나지 않아 함수형 컴포넌트에 사용할 수 있는 다양한 훅 API가 추가되었다.

 

function useCounter (){
	const [count,setCount] = useState(0)
    
    function increase(){
    	setCount((prev) => prev + 1)
    }
    
    return {count,increase}
}

 

 

이러한 훅은 클래스형 컴포넌트보다 훨씬 간편하고 직관적인 방법이었다. 이런 hook과 state의 등장으로 이전에 볼 수 없던 상태 관리가 등장하는데 이게 바로 React Query와 SWR이다.

 

두 라이브러리 모두 외부에서 데이터를 불러오는 fetch를 관리해주는 라이브러리이다. SWR을 사용한 코드를 보자.

 

import React from "react";

const fetcher = (url) => fetch(url).then((res) => res.json());

const MyComponent = () => {
  const { data, error } = useSWR(
    "https://api.github.com/repos/vercel/swr",
    fetcher
  );

  if (error) return "An error has occurred";
  if (!data) return "Loading...";

  return (
    <div>
      <p>{JSON.stringify(data)}</p>
    </div>
  );
};

export default MyComponent;

 

 

 

 

 

Recoil,zustand,Jotai,Valtio,...

 

훅이라는 새로운 패러다임 이후 다양한 라이브러리들이 만들어졌다 페이스북 팀에서 만든 Recoil을 필두로 다양한 라이브러리들이 나왔다.

 

//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)

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

 

 

 

따라서 현재 다양한 상태관리 라이브러리가 있는 만큼 상태관리 라이브러리를 선택하는 경우 다양한 옵션을 살펴보고 골라보자.

 

끄읏 -!

 

 

 

 

 

 

 

728x90