현재 오즈코딩스쿨 강의를 통해 프론트엔드를 학습하고 있습니다.
본 포스트는 해당 강의에 대한 내용 정리를 목적으로 합니다.
React 상태 관리 개념은 컴포넌트 내에서 데이터를 어떻게 관리하고 전달할지에 대한 개념이다. React는 기본적으로 "단방향 데이터 흐름"을 따르며, 상태를 관리하는 방식에 따라 애플리케이션의 복잡도를 줄이고, 유지보수성을 높일 수 있다.

Props Drilling은 React에서 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 때 발생하는 문제를 의미한다. 즉, 상위 컴포넌트에서 하위 컴포넌트로 여러 단계에 걸쳐 props를 전달해야 하는 상황을 말한다. 이 방식은 특히 컴포넌트 계층이 깊어질수록 코드가 복잡해지고 관리가 어려워지는 단점이 있다.


function Grandparent() {
const [count, setCount] = useState(0);
return <Parent count={count} setCount={setCount} />;
}
function Parent({ count, setCount }) {
return <Child count={count} setCount={setCount} />;
}
function Child({ count, setCount }) {
return (
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
이 예시에서 Grandparent 컴포넌트에서 count와 setCount를 정의하고, Parent 컴포넌트를 통해 Child 컴포넌트로 전달한다. 즉, 데이터가 여러 단계의 컴포넌트를 거쳐 전달되므로 props drilling이 발생한다.
문제점
코드가 길어지고 복잡해짐: 컴포넌트 간 데이터 전달이 많아지면, 데이터 전달 코드가 많아져 관리하기 어려워진다.
불필요한 리렌더링: 상위 컴포넌트의 상태 변화가 깊은 자식 컴포넌트에 전달될 때, 중간 컴포넌트들이 불필요하게 리렌더링될 수 있다.
컴포넌트 간의 의존성 증가: 중간 컴포넌트가 단지 데이터를 전달하는 역할만 하더라도, 해당 컴포넌트가 부모와 자식 컴포넌트와 강하게 연결되기 때문에 의존성이 증가한다.
해결 방법
Context API를 사용하면 props drilling을 피할 수 있다. Context는 데이터를 컴포넌트 트리 전체에 전달할 수 있게 해준다. 부모에서 하위 컴포넌트로 직접 전달하는 방식이 아니라, 필요한 컴포넌트가 useContext를 사용하여 값을 가져온다.import { createContext, useContext, useState } from 'react';
const CountContext = createContext();
function Grandparent() {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
<Parent />
</CountContext.Provider>
);
}
function Parent() {
return <Child />;
}
function Child() {
const { count, setCount } = useContext(CountContext);
return (
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
이 방식에서는 Grandparent 컴포넌트가 CountContext.Provider로 count와 setCount를 제공하고, 하위 컴포넌트는 useContext를 통해 이 값을 직접 가져온다. 이렇게 하면 중간 컴포넌트들이 props를 전달할 필요가 없다.
상태 끌어올리기 (Lifting State Up)
때로는 상태를 중간 컴포넌트로 끌어올려서, 필요한 자식 컴포넌트에 props로 전달하는 방식도 해결책이 될 수 있다. 이 방식은 props drilling을 줄이기 위한 방법이지만, 여전히 컴포넌트 간의 데이터 전달을 위한 구조가 필요하다.
상태 관리 라이브러리 사용
애플리케이션이 복잡해지면, Redux, Recoil, Zustand와 같은 상태 관리 라이브러리를 사용하여 전역 상태 관리를 할 수 있다. 이렇게 하면, 컴포넌트 간에 데이터를 전달할 때 props drilling을 피할 수 있다.
여러 컴포넌트 간 데이터 공유: 여러 컴포넌트에서 동일한 상태를 필요로 할 때 유용하다.
상태 전달 복잡도 감소: props drilling이나 상태를 끌어올리는 복잡성을 줄여준다.
컴포넌트 독립성 향상: 전역 상태로 데이터를 관리하면 중간 컴포넌트들이 데이터를 전달할 필요가 없다.
상태 공유 용이: 여러 컴포넌트에서 쉽게 상태를 공유할 수 있다.
코드 간결성: props drilling을 피할 수 있어 코드가 깔끔하다.
성능 최적화: 필요한 컴포넌트만 리렌더링하도록 최적화할 수 있다.
중앙 집중식 관리: 상태를 한 곳에서 일괄적으로 관리해 디버깅이 쉬워진다.
가장 기본적인 상태 관리 방법은 useState 훅을 사용하는 것이다. 이 방법은 각 컴포넌트 내부에서 상태를 정의하고 관리하는 데 사용된다.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
useState는 상태 변수(count)와 그 값을 변경할 수 있는 함수(setCount)를 반환한다.useReducer는 useState보다 더 복잡한 상태 관리에 유용하다. 주로 여러 값에 따라 상태가 변경되는 경우, 복잡한 로직을 처리할 때 사용된다. reducer 함수와 dispatch 함수를 사용해 상태를 업데이트한다.
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>현재 카운트: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>증가</button>
<button onClick={() => dispatch({ type: 'decrement' })}>감소</button>
</div>
);
}
useReducer는 상태 변화가 여러 단계로 복잡한 경우에 유리하다.dispatch 함수로 액션을 보내어 상태를 업데이트한다.useContext와 Context API를 사용하면 상태를 여러 컴포넌트 간에 공유할 수 있다. Context API는 전역 상태 관리 방법으로, 상태를 하나의 공급자(Provider)를 통해 제공하고, 자식 컴포넌트에서 이를 소비할 수 있다.
import React, { useContext, useState, createContext } from 'react';
const CountContext = createContext();
function App() {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
<Counter />
</CountContext.Provider>
);
}
function Counter() {
const { count, setCount } = useContext(CountContext);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
CountContext.Provider로 상태를 하위 컴포넌트들에 공급하고, useContext로 이를 소비한다.useRef는 렌더링과 관련 없는 값을 저장하거나, DOM 요소에 접근하는 데 사용된다. 상태값을 변경하더라도 컴포넌트를 리렌더링하지 않고 값을 유지할 수 있다.
import { useRef } from 'react';
function Timer() {
const timerRef = useRef(0);
const startTimer = () => {
timerRef.current = setInterval(() => {
console.log('타이머:', timerRef.current);
}, 1000);
};
return <button onClick={startTimer}>타이머 시작</button>;
}
useRef는 상태를 관리하지만, 변경 시 리렌더링을 일으키지 않기 때문에 렌더링 성능에 영향을 미치지 않는다.Context API는 React에서 컴포넌트 트리 전체에 데이터를 전역적으로 전달할 수 있도록 도와주는 기능이다. props drilling 문제를 해결할 수 있어, 컴포넌트 간에 상태나 데이터를 공유할 때 매우 유용하다.
Context 생성: createContext로 Context를 만들고, 데이터를 담을 수 있는 Provider를 설정한다.
Provider: 하위 컴포넌트에 데이터를 전달하는 역할을 한다.
Consumer: 하위 컴포넌트에서 useContext를 사용하여 데이터를 접근한다.
import React, { useState, createContext, useContext } from 'react';
// Context 생성
const CountContext = createContext();
function App() {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
<Counter />
</CountContext.Provider>
);
}
function Counter() {
const { count, setCount } = useContext(CountContext);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
React 외부에서 상태를 관리하는 방법은 컴포넌트 내부가 아닌, 애플리케이션 전체에서 전역 상태를 다루기 위해 외부 라이브러리를 사용하는 방식이다
가장 널리 사용되는 상태 관리 라이브러리.
상태를 중앙의 store에 저장하고, dispatch로 상태를 변경하며, reducer로 변경 로직을 정의한다.
예측 가능한 상태 관리가 가능하다.
// 기본 개념
store → 상태 저장소
dispatch → 액션 전송
reducer → 상태 변경 로직
Facebook에서 만든 상태 관리 라이브러리.
React의 상태 관리와 유사한 API (atom, selector)를 제공해 진입 장벽이 낮다.
컴포넌트 단위에서 상태를 선언적으로 다룰 수 있다.
atom → 상태 단위
selector → 파생 상태(계산)
작고 가벼운 상태 관리 라이브러리.
코드가 간단하고, 보일러플레이트가 거의 없다.
useStore 훅 하나로 상태 읽기/변경 모두 가능.
관찰 기반 상태 관리. 상태가 바뀌면 자동으로 관련 컴포넌트가 리렌더링된다.
상태 흐름이 자연스럽고 유연하지만, 구조 파악이 어려울 수 있다.
Redux Toolkit은 Redux의 공식 유틸리티 라이브러리로, Redux를 더 간편하고 효율적으로 사용할 수 있게 해주는 도구 모음이다. 복잡하고 반복적인 코드(boilerplate)를 줄이고, 실수 없이 상태 관리를 할 수 있도록 도와준다.
configureStore
createSlice
createAsyncThunk
pending, fulfilled, rejected 상태 자동 관리createReducer / createAction
- 리듀서나 액션을 간단하게 생성 (보통 createSlice로 대체됨)
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
// 사용 예 (React 컴포넌트)
import { useSelector, useDispatch } from 'react-redux';
import { increment } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch(increment())}>+1</button>
</div>
);
}
보일러플레이트 코드 최소화
비동기 처리 간편화 (createAsyncThunk)
immer 내장 → 상태 불변성 쉽게 유지 가능
기본 DevTools 설정 포함
생산성과 유지보수성 향상