[ React ] 전역 상태 관리는 언제 해야할까?

꾸개·2024년 5월 29일
1

React

목록 보기
6/9
post-custom-banner

개요

리액트를 사용하는 프론트엔드 개발자라면 useState훅을 사용하면서 상태를 이용하고 이를 변수처럼 사용하면서 상태를 갱신하고 컴포넌트가 효율적인 렌더링이 되게끔 개발을 했을 것이다. 이렇게 컴포넌트 내에서 사용하는 상태를 지역상태라고 표현한다.
지역 상태가 있으면, 전역상태도 있을텐데 전역상태는 어떨 때 사용하는걸까?

상태

먼저, 상태라는 개념에 대해 정확히 짚고 넘어갈 필요가 있다. 리액트 공식문서에서 알려주는 State는 다음과 같다.

  • state(상태)의 정의보다 어떨 때 state를 써야하는지 알려준다.
  • 컴포넌트가 렌더링 간의 정보를 추적해야 하는 경우 컴포넌트 자체가 상태를 생성, 업데이트 및 사용할 수 있다.

https://lucybain.com/blog/2016/react-state-vs-pros/
https://ko.legacy.reactjs.org/docs/faq-state.html#gatsby-focus-wrapper

상태의 사용법에 따라 상태를 정의하자면, 컴포넌트 내부에서 관리되는 데이터로, 컴포넌트의 렌더링 및 동작을 제어하는 요소 라고 할 수 있다.
리액트는 컴포넌트 기반으로 동작하고 각 컴포넌트는 가상 돔(Virtual DOM)에 의해 독립적으로 렌더링 될 수 있다. 따라서 각 컴포넌트에 상태를 갖고, 그 상태를 추적하면서 컴포넌트는 상태에 따라 렌더링 여부를 결정할 수 있다.

prop

또한 이 상태는 상속이 가능하다. 리액트에서의 상속은 prop으로 이루어지는데 컴포넌트에 직접 추가해주면 상속을 받을 수 있다.

import { useState } from React



const ChildComponent = (props) => { // or { state }
	const state = props.state
	return <div>{state}</div>
}


const App = () => {
	const [state, setState] = useState('state')
    
    return(
    	<div>
      		<ChildComponent state={state}/>
      	</div>
    )

}

props-drilling

한 두 개의 컴포넌트를 통해 props을 내리는것은 괜찮다. 하지만 하위 컴포넌트가 더 많다면? 가독성을 해치고 유지보수가 어려울 수 있다. 이렇게 props를 내리꽂는 현상을 props-drilling이라 한다. 영상을 보면 좀 더 쉽게 이해할 수 있다.


전역상태

이러한 문제를 해결하고자 탄생한 개념이 전역상태인데, 전역 상태는 말 그대로 어느 컴포넌트에서든지 prop을 상속받지 않아도 상태를 추적할 수 있다.

  • 현재 대부분의 전역 상태 관리 라이브러리들은 Flux 아키텍처의 데이터 단방향 흐름을 지향하는데 이는 Redux가 최초로 고안한 기법이다.
  • Redux외 다른 전역상태 관리 라이브러리들도 해당 그림과 같은 개념을 사용한다.

FLux란?
Flux는 Facebook에서 만든 아키텍처로, 데이터의 단방향 흐름을 강조한다.
1. Action: 상태 변경을 요청하는 객체로, 무엇이 일어날지를 설명
2. Dispatcher: 모든 액션을 받아서 스토어로 전달하는 중앙 허브 역할
3. Store: 애플리케이션의 상태와 상태 변경 로직을 담고 있는 객체
4. View: 상태를 기반으로 사용자 인터페이스(UI)를 렌더링하는 컴포넌트

props-drilling을 제외하고도 어떠한 정보가 리액트 외부에 있을 때와 같이 prop을 전달하는 것이 불필요할 때 전역상태를 사용한다. 따라서 전역상태를 사용하는 경우는 2가지로 꼽을 수 있다.

  • props-drilling 혹은 prop을 전달하는 것이 불필요할 때
  • 상태가 이미 리액트 외부에 있을 때

아주 간단한 예를 든다면 다크모드를 구현한다고 가정해보자. 다크 모드는 이용자가 다크모드를 실행시키면 모든 UI가 다크모드에 맞게 변경된다. 이 때, 컴포넌트가 여러개라면은 모든 컴포넌트에 prop을 전달해줘야 한다는 불편함이 있다.

import React, { useState } from 'react';

const App = () => {
  const [isDarkMode, setIsDarkMode] = useState(false);

  const toggleDarkMode = () => {
    setIsDarkMode(prevMode => !prevMode);
  };

  return (
    <div className={isDarkMode ? 'dark-mode' : 'light-mode'}>
      <h1>{isDarkMode ? 'Dark Mode' : 'Light Mode'}</h1>
      <button onClick={toggleDarkMode}>
        {isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
      </button>
      <Component1 isDarkMode={isDarkMode} />
      <Component2 isDarkMode={isDarkMode} />
    </div>
  );
};

const Component1 = ({ isDarkMode }) => {
  return (
   <div style={{
      backgroundColor: isDarkMode ? '#333' : '#fff',
      color: isDarkMode ? '#fff' : '#333'
    }}>
      <p>
        loremIpsum
      </p>
    </div>
  );
};

const Component2 = ({ isDarkMode }) => {
  return (
   <div style={{
      backgroundColor: isDarkMode ? '#333' : '#fff',
      color: isDarkMode ? '#fff' : '#333'
    }}>
      <p>
        loremIpsum
      </p>
    </div>
  );
};


...

또한, 이미 상태가 리액트 외부에 있다면 지역 컴포넌트로 변경 후 prop을 전달할 필요가 없다.

예를들어, 로컬 스토리지로 다크모드를 구현하던가, 모달 쿠키 값, 데이터 패칭값들을 전역 상태로 받으면 따로 prop로 전달할 필요 없이 구현 가능하다.

// Recoil
import { atom, useRecoilState } from 'recoil';

const darkModeState = atom({
  key: 'darkModeState',
  //로컬 스토리지 값을 불러옴
  default: JSON.parse(localStorage.getItem('darkMode')) || false,
});

// 다크 모드를 사용하는 컴포넌트
const DarkModeToggle = () => {
  const [darkMode, setDarkMode] = useRecoilState(darkModeState);
 
  // 다크 모드 상태가 변경될 때 로컬 스토리지에 저장
  useEffect(() => {
    localStorage.setItem('darkMode', JSON.stringify(darkMode));
  }, [darkMode]);
 
  return (
    <button onClick={() => setDarkMode(!darkMode)}>Toggle Dark Mode</button>
  );
};

const App = () => {
  const [darkMode] = useRecoilState(darkModeState);

  return (
    <div className={darkMode ? 'dark-mode' : 'light-mode'}>
      <DarkModeToggle />
    </div>
  );
};
  • 로컬 스토리지 값을 전역으로 갖고오기

마이크로 상태관리

리액트 초기에는 리액트 자체에서 전역 상태를 관리하는 형식을 제공하지 않아, 많은 라이브러리들이 개발되었다. 특히, Redux는 초반에 많은 각광을 받았다.

시간이 지난 후, Redux의 단점이 부각되기 시작했다. 가파른 러닝커브와 보일러 플레이팅이 굉장히 많은 Redux의 코드는 개발자들에게 피로를 안겼고 그에 따라 마이크로 상태 관리 개념이 부상하게 되었다.

마이크로 상태관리란?

  • 컴포넌트 단위에서 로컬 상태를 관리하고, 필요에 따라 적절하게 전역 상태와 결합하는 방식이다.
  • 이는 애플리케이션의 상태 관리 복잡성을 줄이고, 상태 관리를 보다 직관적이고 간단하게 하기 위한 접근법이다.
  • 이를 위해 경량화 된 라이브러리들을 사용한다.

경량화 된 라이브러리들은 상태를 어떻게 관리할 것인지 구체적인 의견을 많이 제시하지 않는다. 경량화 된 라이브러리들은 다음과 같다.

  • Redux => Zustand
    • Zustand는 Redux에서 많은 영감을 받았고, 그에 따라 경량화를 했다. 사용법이 많이 유사하지만 리듀서를 기반으로 하지 않는다.
  • Recoil => Jotai
    • Jotai는 Reocil에서 많은 영감을 받았고, 그에 따라 경량화를 했다.
      key 프로퍼티가 없다는 것이 특징이고, 선택자 기반이 아닌 렌더링 최적화를 위한 최소한의 API를 제공한다.
  • MobX => Valtio
    • 철학은 다르지만, 사용방법에서 몇 가지 유사한 점이 있다. Valtio는 proxy를 사용해서 중계를 이용해 상태관리 한다.

이 3개의 라이브러리들은 놀랍게도 'Poimandres' 라는 모두 같은 집단에서 개발했다.


경량화 된 세 라이브러리의 차이점은 무엇일까?

이 세가지의 라이브러리는 두 가지의 관점으로 차이점을 살펴 볼 수 있다.

상태가 어디에 위치하는가?

  • 모듈: 리액트 생명주기와 독립적으로 존재하는 상태로, 보통 리액트 외부에서 정의되고 관리된다. 이는 리액트와 무관하게 독립적으로 업데이트되고 유지될 수 있다.
  • 컴포넌트: 리액트 컴포넌트 내에서 관리되며, 리액트의 생명주기와 밀접하게 연관되어 있다, 컴포넌트가 생성되거나 소멸될 때 상태도 함께 생성되고 소멸된다.

Zustand, Valtio

  • 상태 위치: 모듈 상태
  • 상태는 리액트 컴포넌트 외부에서 정의되며, 리액트와 무관하게 업데이트되고 유지된다. 상태를 글로벌로 정의하여 필요할 때마다 리액트 컴포넌트에서 접근할 수 있다.

Jotai

  • 상태 위치: 컴포넌트 상태
  • atom을 통해 상태를 정의하고, 리액트 컴포넌트가 이를 구독한다. 상태는 리액트 컴포넌트 생명주기와 연동되어 관리된다.

따라서, 리액트 생명주기와 관련 없이 독립적으로 전역 상태를 관리하려면 Zustand, Valtio를 전역상태를 계속해서 추적하려면 구독 관리 방식을 따르는 Jotai를 사용할 것을 권장한다.


상태 갱신 스타일이 무엇인가?

  • 불변: 상태를 변경할 때마다 새로운 상태 객체를 생성하여 기존 상태 객체는 변경하지 않는 방식
  • 변경가능: 상태 객체를 직접 변경하여 업데이트하는 방식

Zustand, Jotai

  • 갱신 스타일: 불변
  • 상태를 변경하려면 객체를 새로 생성해야 하기 때문에 객체 참조를 비교해서 변경 사항이 있는지 파악할 수 있다. 규모가 크고 중첩된 객체의 성능을 향상시키는데 도움이 된다.

Valtio

  • 갱신 스타일: 변경 가능
  • 객체가 깊이 중첩된 경우에 편리하다. 상태 관리가 직관적이고 간편하며, 코드의 양을 줄일 수 있다.

따라서, 규모가 크고 성능을 향상 시켜야 하는 어플리케이션이면 Zustand 혹은 Jotai, 상태 관리를 직관적으로 하고 유지보수 측면을 고려해야 한다면 Valtio를 선택할 수 있다.


마치며

리액트 훅을 활용한 마이크로 상태관리 책을 읽으며 상태와 전역 상태 관리 그리고 그 라이브러리들을 어떻게 사용하고 최적화 하는지 공부해보았다. 확실히 책으로 정보를 접하니 내용에 신뢰가 가고 깔끔하게 정리되는 기분이었다. 하지만, 자주 접하던 코드들이 아니다 보니 코드레벨이 높게 느껴졌다. 이번에는 전역 상태 관리에만 포커스를 맞췄다면, 다음에는 각 라이브러리들을 어떻게 사용하는지 비교해보고 나의 최적 라이브러리 혹은 전역 상태관리 방법을 찾아보아야겠다.

끝으로 공부하며 새롭게 알게 된 지식과 생각들을 추가로 작성해 보았다.

참고
https://product.kyobobook.co.kr/detail/S000212233308
https://f-lab.kr/insight/react-state-management-evolution
https://yozm.wishket.com/magazine/detail/2233/

추가글

props-drilling은 죄악이다?

블로그를 통해 공부하면서 props-drilling을 죄악처럼 여기고 안티패턴처럼 소개하는 글이 많았는데 꼭 그렇지는 않다.

1. 재사용성 용이

같은 prop을 받는 컴포넌트를 새로 생성할 필요 없이 재사용이 가능하다. 예를 들어, 리스트를 만들어야 할 때 prop으로 상위 컴포넌트의 데이터를 각 리스트에 뿌려주기만 하면 되기에 재사용이 용이하다.

2. 데이터가 단순하다.

리액트 외부의 데이터가 아니기에 데이터가 단순할 확률이 아주 높다.

3. 데이터 유지보수가 쉽다.

오히려 뎁스가 깊지 않다면 전역 상태 관리를 사용하는 것보다 데이터 흐름이 직관적이어서 더 쉬울 수 있다.

4. 전역 상태 관리를 하지 않아도 된다.

아이러니 하게도, 전역 상태 관리를 하지 않는것이 장점이 될 수 있다. 보일러플레이트 코드를 작성하고 그에 따른 에러와 유지보수 비용을 줄일 수 있다.

그렇다면 뭐 어쩌라고?

실컷 전역 상태에 대해 설명해놓고 이제 와서 어쩌자는 거냐라고 생각 할 수 있지만, 본인만의 기준이 중요한 것 같다. 뎁스가 3이상이 되거나, 다른 뎁스에 있는 상태를 가져오려면 전역 상태를 사용할 수 있다.

혹은 애초에 컴포넌트를 너무 애자일하게 설정한게 아닌지 생각해 볼 필요도 있다. 유지보수를 위해 컴포넌트를 잘게 자른것이 주객이 전도되어 props-drilling을 발생시켜 유지보수가 어렵게 되는것은 아닌지 생각해 볼 필요가 있다.

필자는 전역상태는 전역변수다. 라는 생각을 기본적으로 갖고 있어 최대한 사용을 지양하는 편이다. 하지만, 코드가 놀랍도록 가독성이 좋아지거나 성능이 좋아질 가능성이 있다면 고려하는 편이다.

profile
내 꿈은 프론트 왕
post-custom-banner

0개의 댓글