본문 바로가기
3주차

다양하게 상태 관리 하기 💿

by LOCOCO 팀블로그 2025. 5. 2.

안녕하세요 SOPT WEB파트 YB 김정은입니다! 👋

setState로 시작해 라이브러리까지, 프론트엔드 개발자들은 더 나은 상태관리 방식을 찾기 위해 고민해오고 있어요. 정말 많은 상태관리 라이브러리가 있는데 비교해 보고 어떤 게 적절할지 알아보려고 해요.

 

상태 (state)

상태는 react에서 동적으로 변경되는 값을 의미하고 애플리케이션을 랜더링 하는데 영항을 미치게 돼요. 전역 상태, 컴포넌트 간 상태, 지역 상태로 나누어져 있어요.

 

상태 관리의 필요성

  • Props를 통해 상태 공유 -> 자식 컴포넌트 간에는 상태 공유 불가능
  • 컴포넌트 계층이 많아지면 전달하는 컴포넌트가 많아짐 -> 사용할 필요 없는 컴포넌트에도 데이터 전달 -> Props의 출처를 찾기 어려움

비효율적인 pros drilling

  • 상태 변경 추적 문제 -> 언제, 어디서 상태가 변경되었는지 파악하기 어려움
  • 상태가 변경되면 해당 Props를 갖고 있는 상태들은 리렌더링 -> 성능 저하
  • 애플리케이션 전체에서 필요한 데이터 관리를 쉽게 하여 중복 코드를 방지하고 재사용성이 증가

이러한 문제들을 해결하기 위해 다양한 상태 관리 도구들이 등장했어요.

 

Context API

React의 내장 기능인 Context API는 컴포넌트 트리를 통해 여러 컴포넌트 간에 전역 상태를 공유해요.

 

장점

  • 추가 설치 없이 React에서 기본적으로 제공
  • 어렵지 않게 핸들링 가능
  • 추가 라이브러리 없이 구현 가능

단점

  • Context 값이 변경될 때마다 해당 Context를 구독하는 모든 컴포넌트가 리렌더링 -> 성능 저하
  • 상태 로직 분리 어려움 -> 복잡한 상태 로직을 관리하기에는 제한적
// Context 생성
const ThemeContext = React.createContext('light');

// 하위 컴포넌트를 Provider로 감싸기
function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <MainComponent />
    </ThemeContext.Provider>
  );
}

// 사용하기
function ThemedButton() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
      style={{ backgroundColor: theme === 'light' ? '#ffffff' : '#000000'}
    >
      테마 토글
    </button>
  );
}

 

상위에서 컴포넌트를 Provider로 감싸주면 하위 컴포넌트에서 상태를 받아 사용할 수 있어요. Context API는 낮은 빈도로 값이 변하는 경우에 사용하는 게 적절해요.

 

Redux

가장 처음 등장한 상태관리 라이브러리로 지금까지도 많은 개발자들이 사용중이에요. 어플리케이션 전체에서 중앙 상태 관리 방식으로 동작하고 리액트를 사용하지 않는 vanila JS에서도 사용이 가능해요.

 

action -> dispatch -> reducer -> view 흐름으로 아래 사진처럼 동작해요.

 

장점

  • 단방향 데이터 흐름으로 상태 변화를 추적하기 쉬움
  • 비동기 작업, 로깅 등을 위한 확장 가능한 구조
  • 대규모 커뮤니티가 존재하여 풍부한 문서, 패턴, 생태계 지원

단점

  • 많은 양의 코드 작성 필요(보일러플레이트 코드) -> Redux Toolkit으로 보완
  • 비동기 처리의 복잡성 -> redux-thunk, redux-saga 등 추가 라이브러리 필요
// 액션 타입
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// 액션 생성자
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });

// 리듀서
const counterReducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const store = createStore(counterReducer);

// 사용하기
function Counter() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();
  
  return (
    <div>
      <button onClick={() => dispatch(decrement())}>-</button>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
}

 

Recoil

Facebook에서 개발한 React 상태관리 라이브러리로 React의 동시성 모드와 함께 작동하도록 설계됐어요.

atom이라는 단위를 사용해서 atom에 변화가 생기면 해당 atom을 사용하는 모든 컴포넌트들이 재렌더링 돼요.

 

atom -> selector -> view 흐름으로 동작

상태마다 Atom이 존재

장점

  • React에 최적화되어 React의 새로운 기능을 잘 활용
  • 원자(Atom) 기반 접근 -> 상태를 작은 단위로 관리하여 불필요한 리렌더링 방지
  • 선택자(Selector)를 통해 파생 상태를 효율적으로 계산
  • 비동기 상태 관리를 위한 API 제공

단점

  • 적은 미들웨어 지원 -> 일부 고급 사용 사례에서 제한적
  • 메모리 누수 발생 사례

 

⚠️ Recoil을 간단하게 작성한 이유는 React 19에서는 사용할 수 없고 업데이트를 중단했기 때문에 사용을 지양해야 해요 😿 

⚠️ 대신에 비슷하게 동작하는 라이브러리인 Jotai로 대체할 수 있어요.

 

Jotai

React를 위한 상태관리 라이브러리로 Recoil과 유사한 방법을 사용해요. atom 단위로 사용하고 상태가 변경된 경우 해당 atom을 사용하는 컴포넌트들만 재렌더링 돼요.

 

  • 작은 번들 크기를 갖고 외부 의존성이 매우 낮음
  • React 철학과 맞는 라이브러리
  • 최소한의 API -> 완만한 러닝커브를 가지며 보일러플레이트 코드 최소화
// atom 생성
import { atom } from 'jotai';

export const counterAtom = atom(0);

// 사용하기
import { useAtom } from 'jotai';
import { counterAtom } from './atoms/counterAtom';

function App() {
  const [count, setCount] = useAtom(counterAtom);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(prev => prev + 1)}>+</button>
      <button onClick={() => setCount(prev => prev - 1)}>-</button>
    </div>
  );
}

export default App;

 

Zustand

Zustand는 간결함과 유연성에 중점을 둔 경량 상태 관리 라이브러리로 Redux의 개념을 단순화하여 가볍게 사용할 수 있어요.

 

  • 유연한 사용으로 완만한 러닝 커브
  • 불필요한 렌더링 최소화
  • Provider 필요 없이 단순하게 사용 가능
  • 가장 작은 번들 사이즈
// 상태 선언
import { create } from 'zustand'

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 })
}));

// 사용하기
function Counter() {
  const { count, increment, decrement, reset } = useStore();

  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
      <button onClick={reset}>초기화</button>
    </div>
  );

}

 

 


 

상태 관리는 프론트엔드 개발에서 중요하기 때문에, 각 라이브러리의 장단점을 이해하고 프로젝트의 규모와 팀의 개발 방식 등 여러 환경에 적합한 도구를 선택하는 것이 중요해요.

npm에서 라이브러리 별 다운로드된 수를 표로 확인할 수 있어요. 라이브러리의 크기와 최근 업데이트 날짜도 함께 확인하여 선택하는 게 좋아요. 해당 라이브러리가 지원을 잘해주는지 확인할 수 있어요. 😉

 

Jotai와 Recoil은 Atomic 패턴, Zustand와 Redux는 Flux 패턴을 사용해요.

Flux 패턴은 중앙 store를 갖고, 내부에서 상태를 정의하고 관리해요.

Atomic 패턴은 분산형 방식으로 컴포넌트에 상태가 분산되어 Atom 단위로 사용해요.

 

대규모 프로젝트, Redux에서 마이그레이션을 진행하는 경우라면 Zustand, 비교적 작은 프로젝트며 Recoil를 사용했다면 Jotai를 사용하는 것을 권장드려요 :)

상태관리 방법들에 대해 간략하게 알아봤는데 상황에 적절하게 선택해서 사용하면 좋을 것 같아요!