React(리액트) 데이터 흐름: Props 드릴링부터 상태 관리 까지

3년전 작성했던 : 부모 -> 자식에게 함수 전달(props 전달) -> 자식 -> 부모에게 상태 전달 (state lifting) 에 관한 글에 대한 좀더 추가된 내용으로 작성한 글 입니다.
📚 목차
- 소개: 리액트의 데이터 흐름
- Props 드릴링: 이해와 활용
- State Lifting: 상태 끌어올리기
- Props 드릴링과 State Lifting의 한계
- 상태 관리 기법
- 핵심정리
소개: 리액트의 데이터 흐름
리액트는 단방향 데이터 흐름(Unidirectional Data Flow)을 따릅니다. 이 개념은 리액트를 처음 접하는 많은 개발자에게 생소하면서도 핵심적인 부분입니다. 그렇다면 이 데이터 흐름이 실제로 어떻게 작동하는지 살펴보겠습니다.
리액트의 데이터는 부모 컴포넌트에서 자식 컴포넌트로 단방향으로 흐릅니다. 이 흐름을 가장 잘 설명해주는 두 가지 핵심 패턴이 바로 **Props 드릴링(Props Drilling)**과 **State 리프팅(State Lifting)**입니다.
단방향 데이터 흐름은 리액트 애플리케이션을 예측 가능하고 디버깅하기 쉽게 만들어 줍니다. 상태가 어디서 오고 어디로 가는지 추적하기 쉽기 때문이죠. 하지만 애플리케이션이 커지면서 이 단순한 흐름이 복잡해질 수 있습니다.
이 글에서는 단방향 데이터 흐름의 두 핵심 패턴인 Props 드릴링과 State 리프팅에 대해 알아보고, 이 패턴들의 한계점과 최신 상태 관리 기법들까지 알아보겠습니다.
Props 드릴링: 이해와 활용
Props 드릴링이란?
Props 드릴링은 상위 컴포넌트에서 하위 컴포넌트로 데이터(props)를 전달하는 과정에서, 중간에 있는 여러 컴포넌트들을 거쳐 최종 목적지인 컴포넌트에 도달하는 패턴을 말합니다.
예를 들어, App → Layout → Sidebar → Menu → MenuItem 구조에서 App의 상태를 MenuItem에 전달하려면, 그 사이의 모든 컴포넌트들이 해당 props를 받아 다음 컴포넌트로 전달해야 합니다.
Props 드릴링이 유용한 경우
- 단순한 컴포넌트 계층 구조: 컴포넌트 depth가 깊지 않은 경우
- 명확한 데이터 흐름: 데이터가 어디서 오는지 추적하기 쉬움
- 소규모 애플리케이션: 상태 관리가 복잡하지 않은 경우
- 특정 경로로만 데이터가 필요할 때: 모든 컴포넌트가 아닌 특정 경로의 컴포넌트들만 데이터가 필요한 경우
Props 드릴링 실제 예제
App.tsx
// App.tsx
import React, { useState } from 'react';
import ProfileSection from './ProfileSection';
// 사용자 타입 정의
interface User {
id: string;
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
const App: React.FC = () => {
// 사용자 상태 정의
const [user, setUser] = useState<User>({
id: 'user-1',
name: '김리액트',
email: 'react@example.com',
preferences: {
theme: 'light',
notifications: true,
}
});
// 테마 변경 핸들러
const handleThemeChange = (theme: 'light' | 'dark') => {
setUser(prevUser => ({
...prevUser,
preferences: {
...prevUser.preferences,
theme
}
}));
};
return (
<div className="app">
<h1>사용자 프로필</h1>
{/* user와 handleThemeChange를 ProfileSection으로 드릴링 */}
<ProfileSection
user={user}
onThemeChange={handleThemeChange}
/>
</div>
);
};
export default App;
App.tsx -> ProfileSection.tsx
// ProfileSection.tsx
import React from 'react';
import ProfileCard from './ProfileCard';
interface User {
id: string;
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
interface ProfileSectionProps {
user: User;
onThemeChange: (theme: 'light' | 'dark') => void;
}
const ProfileSection: React.FC<ProfileSectionProps> = ({ user, onThemeChange }) => {
return (
<section className="profile-section">
<h2>프로필 섹션</h2>
{/* user와 onThemeChange를 더 깊은 컴포넌트로 전달 */}
<ProfileCard
user={user}
onThemeChange={onThemeChange}
/>
</section>
);
};
export default ProfileSection;
App.tsx -> ProfileSection.tsx -> ProfileCard.tsx
// ProfileCard.tsx
import React from 'react';
import ThemeToggle from './ThemeToggle';
interface User {
id: string;
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
interface ProfileCardProps {
user: User;
onThemeChange: (theme: 'light' | 'dark') => void;
}
const ProfileCard: React.FC<ProfileCardProps> = ({ user, onThemeChange }) => {
return (
<div className="profile-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
{/* onThemeChange를 최종 목적지 컴포넌트로 전달 */}
<ThemeToggle
theme={user.preferences.theme}
onThemeChange={onThemeChange}
/>
</div>
);
};
export default ProfileCard;
App.tsx -> ProfileSection.tsx -> ProfileCard.tsx -> ThemeToggle.tsx
// ThemeToggle.tsx
import React from 'react';
interface ThemeToggleProps {
theme: 'light' | 'dark';
onThemeChange: (theme: 'light' | 'dark') => void;
}
const ThemeToggle: React.FC<ThemeToggleProps> = ({ theme, onThemeChange }) => {
return (
<div className="theme-toggle">
<button
onClick={() => onThemeChange(theme === 'light' ? 'dark' : 'light')}
className={`theme-toggle-button ${theme === 'dark' ? 'dark-mode' : 'light-mode'}`}
>
{theme === 'light' ? '다크 모드로 전환' : '라이트 모드로 전환'}
</button>
</div>
);
};
export default ThemeToggle;
이 예제에서 user 객체와 onThemeChange 함수는 App → ProfileSection → ProfileCard → ThemeToggle 로 전달됩니다. ThemeToggle 컴포넌트만 실제로 이 데이터를 사용하지만, 중간의 모든 컴포넌트들도 이 props를 받아서 전달해야 합니다. 이것이 바로 props 드릴링입니다.
State Lifting: 상태 끌어올리기
State Lifting이란?
State Lifting(상태 끌어올리기)은 하위 컴포넌트에서 필요한 상태를 상위 컴포넌트로 "끌어올려" 관리하는 패턴입니다. 이는 여러 형제 컴포넌트가 동일한 상태를 공유해야 할 때 유용합니다.
State Lifting이 유용한 경우
- 형제 컴포넌트 간 상태 공유: 서로 다른 형제 컴포넌트가 동일한 데이터에 접근하거나 수정해야 할 때
- 상태 변경이 여러 컴포넌트에 영향을 미칠 때: 하나의 상태 변경이 여러 컴포넌트의 렌더링에 영향을 미칠 때
- 단일 진실 공급원(Single Source of Truth): 상태를 한 곳에서 관리하여 일관성 유지
- 폼 데이터 관리: 복잡한 폼에서 여러 입력 필드의 상태를 상위 컴포넌트에서 관리
State Lifting 실제 예제
간단한 State Lifting 예제입니다:
// App.tsx - 상위 컴포넌트
import React, { useState } from 'react';
import TodoForm from './TodoForm';
import TodoList from './TodoList';
// 할 일 항목 타입 정의
interface Todo {
id: number;
text: string;
completed: boolean;
}
const App: React.FC = () => {
// 상태를 상위 컴포넌트에서 관리
const [todos, setTodos] = useState<Todo[]>([]);
// 할 일 추가 함수 - 하위 컴포넌트에서 호출됨
const addTodo = (text: string) => {
const newTodo: Todo = {
id: Date.now(),
text,
completed: false
};
setTodos([...todos, newTodo]);
};
// 할 일 완료 상태 토글 함수 - 하위 컴포넌트에서 호출됨
const toggleTodo = (id: number) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return (
<div className="app">
<h1>Todo 앱</h1>
{/* 할 일 입력 폼 - addTodo 함수를 props로 전달 */}
<TodoForm onAddTodo={addTodo} />
{/* 할 일 목록 - todos 상태와 toggleTodo 함수를 props로 전달 */}
<TodoList todos={todos} onToggleTodo={toggleTodo} />
</div>
);
};
export default App;
// TodoForm.tsx - 하위 컴포넌트 1
import React, { useState } from 'react';
interface TodoFormProps {
onAddTodo: (text: string) => void;
}
const TodoForm: React.FC<TodoFormProps> = ({ onAddTodo }) => {
const [text, setText] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (text.trim()) {
// 부모 컴포넌트로부터 전달받은 함수 호출 - 상태 끌어올리기
onAddTodo(text);
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="새 할 일 입력"
/>
<button type="submit">추가</button>
</form>
);
};
export default TodoForm;
// TodoList.tsx - 하위 컴포넌트 2
import React from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoListProps {
todos: Todo[];
onToggleTodo: (id: number) => void;
}
const TodoList: React.FC<TodoListProps> = ({ todos, onToggleTodo }) => {
if (todos.length === 0) {
return <p>할 일이 없습니다.</p>;
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
);
};
export default TodoList;
이 예제에서는 다음과 같은 State Lifting 패턴을 볼 수 있습니다:
- 상위 컴포넌트(App)에서 상태 관리: todos 상태와 이를 조작하는 함수(addTodo, toggleTodo)가 상위 컴포넌트에 정의되어 있습니다.
- 하위 컴포넌트에 함수 전달: 상위 컴포넌트에서 정의한 함수를 props로 하위 컴포넌트에 전달합니다.
- 하위 컴포넌트에서 상태 업데이트 트리거: TodoForm에서 사용자가 새 할 일을 입력하면 props로 받은 onAddTodo 함수를 호출하여 상위 컴포넌트의 상태를 업데이트합니다.
- 상태 변경의 결과가 다른 컴포넌트에 반영: 상태가 업데이트되면, 해당 상태를 props로 받는 TodoList 컴포넌트가 자동으로 새로운 할 일 목록을 표시합니다.
이 패턴의 핵심은 상태 자체는 상위 컴포넌트에서 관리하면서, 상태를 업데이트하는 함수를 하위 컴포넌트에 전달하여 하위 컴포넌트가 상위 컴포넌트의 상태를 간접적으로 업데이트할 수 있게 하는 것입니다.
Props 드릴링과 State Lifting의 한계
Props 드릴링과 State Lifting은 리액트의 기본적인 데이터 흐름 패턴 이지만, 애플리케이션이 커질수록 몇 가지 한계에 직면합니다:
Props 드릴링의 한계
- 코드 복잡성 증가: 중간 컴포넌트들이 불필요한 props를 전달하는 역할만 하게 됨
- 유지보수 어려움: props 이름 변경 시 모든 중간 컴포넌트 수정 필요
- 가독성 저하: 많은 props가 여러 계층을 통과하면 코드 추적이 어려워짐
- 성능 이슈: 중간 컴포넌트들이 props 변경 시 불필요하게 리렌더링될 수 있음
State Lifting의 한계
- 상태 관리 복잡화: 많은 상태가 상위 컴포넌트에 집중되어 복잡해짐
- 컴포넌트 재사용성 감소: 상태가 특정 상위 컴포넌트에 종속되어 컴포넌트 재사용이 어려워짐
- 상태 업데이트 비효율: 작은 상태 변경도 많은 컴포넌트 리렌더링 유발 가능
- 코드 비대화: 상위 컴포넌트에 너무 많은 로직이 집중됨
트러블슈팅: 자주 발생하는 문제와 해결 방법
문제 1: 불필요한 리렌더링
문제 상황: Props 드릴링을 사용할 때 상위 컴포넌트의 상태가 변경되면 모든 중간 컴포넌트들도 리렌더링됩니다.
해결 방법:
- React.memo 사용: 컴포넌트를 React.memo로 래핑하여 props가 변경되지 않으면 리렌더링 방지
- useMemo 및 useCallback 사용: 객체와 함수를 메모이제이션하여 불필요한 리렌더링 방지
// 최적화된 중간 컴포넌트
import React, { memo, useCallback } from 'react';
interface MiddleComponentProps {
passToChild: string;
onChildAction: () => void;
}
const MiddleComponent: React.FC<MiddleComponentProps> = memo(({ passToChild, onChildAction }) => {
console.log('MiddleComponent 렌더링');
return (
<div>
<ChildComponent
data={passToChild}
onAction={onChildAction}
/>
</div>
);
});
// 상위 컴포넌트에서의 사용
const ParentComponent: React.FC = () => {
const [value, setValue] = useState('');
// useCallback으로 함수 메모이제이션
const handleChildAction = useCallback(() => {
console.log('Child action triggered');
}, []);
return (
<div>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<MiddleComponent
passToChild="some data"
onChildAction={handleChildAction}
/>
</div>
);
};
문제 2: State Lifting으로 인한 상위 컴포넌트의 비대화
문제 상황: 너무 많은 상태가 상위 컴포넌트에 집중되어 코드가 복잡해집니다.
해결 방법:
- 상태 로직 분리: 커스텀 훅을 사용하여 상태 로직 분리
- 컴포넌트 분해: 큰 컴포넌트를 더 작은 책임 영역을 가진 컴포넌트로 분리
// 분리된 상태 로직을 위한 커스텀 훅
import { useState, useCallback } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
export const useTodos = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = useCallback((text: string) => {
const newTodo: Todo = {
id: Date.now(),
text,
completed: false
};
setTodos(prev => [...prev, newTodo]);
}, []);
const toggleTodo = useCallback((id: number) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
return {
todos,
addTodo,
toggleTodo
};
};
// App 컴포넌트에서 사용
const App: React.FC = () => {
const { todos, addTodo, toggleTodo } = useTodos();
return (
<div>
<TodoForm onAddTodo={addTodo} />
<TodoList todos={todos} onToggleTodo={toggleTodo} />
</div>
);
};
상태 관리
Props 드릴링과 State Lifting의 한계를 극복하기 위해 다양한 상태 관리 기법이 등장했습니다. 이들은 각자의 특성에 맞는 사용 사례가 있습니다.
1. Context API
Context API는 React 내장 기능으로, 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다. Props 드릴링 없이 필요한 컴포넌트에서 직접 데이터에 접근할 수 있습니다.
// TodoContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoContextType {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
}
// Context 생성
const TodoContext = createContext<TodoContextType | undefined>(undefined);
// Context Provider 컴포넌트
export const TodoProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (text: string) => {
// 새 할일 생성 후 상태 업데이트
const newTodo: Todo = { id: Date.now(), text, completed: false };
setTodos([...todos, newTodo]);
};
const toggleTodo = (id: number) => {
// 할일 완료 상태 토글
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
return (
<TodoContext.Provider value={{ todos, addTodo, toggleTodo }}>
{children}
</TodoContext.Provider>
);
};
// 커스텀 훅으로 Context 사용 편리하게 만들기
export const useTodoContext = () => {
const context = useContext(TodoContext);
if (context === undefined) {
throw new Error('useTodoContext must be used within a TodoProvider');
}
return context;
};
// App.tsx
import React from 'react';
import { TodoProvider } from './TodoContext';
import TodoForm from './TodoForm';
import TodoList from './TodoList';
const App: React.FC = () => {
return (
<TodoProvider>
<div className="app">
<h1>Todo 앱</h1>
<TodoForm />
<TodoList />
</div>
</TodoProvider>
);
};
// TodoForm.tsx - Context를 사용하는 컴포넌트
import React, { useState } from 'react';
import { useTodoContext } from './TodoContext';
const TodoForm: React.FC = () => {
const [text, setText] = useState('');
const { addTodo } = useTodoContext(); // Context에서 함수 가져오기
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (text.trim()) {
addTodo(text);
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="새 할 일 입력"
/>
<button type="submit">추가</button>
</form>
);
};
2. Zustand
Zustand는 간단하고 직관적인 API를 제공하는 상태 관리 라이브러리입니다. Redux와 유사한 개념이지만 보일러플레이트 코드가 적고 TypeScript와의 통합이 우수합니다.
// todoStore.ts
import create from 'zustand';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoStore {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
getTodoCount: () => number; // 파생 상태 계산
}
// Zustand 스토어 생성
export const useTodoStore = create<TodoStore>((set, get) => ({
todos: [],
// 할일 추가 함수
addTodo: (text: string) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }]
})),
// 할일 완료 상태 토글 함수
toggleTodo: (id: number) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
// 파생 상태: 할일 개수 계산
getTodoCount: () => get().todos.length
}));
// TodoList.tsx - Zustand 사용
import React from 'react';
import { useTodoStore } from './todoStore';
const TodoList: React.FC = () => {
// 스토어에서 상태와 함수 직접 가져오기
const todos = useTodoStore(state => state.todos);
const toggleTodo = useTodoStore(state => state.toggleTodo);
const count = useTodoStore(state => state.getTodoCount());
if (todos.length === 0) {
return <p>할 일이 없습니다.</p>;
}
return (
<>
<p>총 {count}개의 할 일이 있습니다.</p>
<ul>
{todos.map(todo => (
<li key={todo.id}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
</>
);
};
상태 관리 장점
- 코드 가독성 향상: 상태 관련 로직이 한 곳에 집중되어 있어 코드를 이해하기 쉽습니다.
- 유지보수성 향상: 상태 변경 로직을 수정할 때 한 곳만 변경하면 됩니다.
- 성능 최적화: 필요한 컴포넌트만 렌더링되도록 최적화할 수 있습니다.
- 테스트 용이성: 상태 로직을 독립적으로 테스트할 수 있습니다.
- 확장성: 애플리케이션이 커져도 상태 관리 구조를 일관되게 유지할 수 있습니다.
상태 관리 라이브러리 선택 가이드
- 소규모 앱: React의 useState와 useReducer로 충분
- 중간 규모 앱: Context API 또는 Zustand
- 대규모 앱: 전역 상태가 많고 복잡하다면 Zustand 또는 Redux
상태 관리는 애플리케이션의 규모와 복잡성에 따라 적절한 방법을 선택하는 것이 중요합니다. 모든 경우에 완벽한 해결책은 없으며, 프로젝트의 요구사항에 맞는 접근 방식을 선택하는 것이 좋습니다.
3. Redux와 Redux-Toolkit
Redux는 React 애플리케이션에서 가장 널리 사용되는 상태 관리 라이브러리입니다. 하지만 기본 Redux는 보일러플레이트 코드가 많아 사용이 복잡하다는 단점이 있습니다. 이를 해결하기 위해 Redux-Toolkit이 등장했습니다.
Redux-Toolkit의 주요 특징
- 보일러플레이트 코드 감소
- 내장된 Immer를 통한 불변성 관리 자동화
- Redux DevTools 기본 설정
- 비동기 작업을 위한 createAsyncThunk 제공
- TypeScript 지원 향상
// todoSlice.ts - Redux Toolkit 사용
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
}
const initialState: TodoState = {
todos: []
};
// 슬라이스 생성 (리듀서 + 액션 통합)
const todoSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// 할일 추가 액션
addTodo: (state, action: PayloadAction<string>) => {
state.todos.push({
id: Date.now(),
text: action.payload,
completed: false
});
// Immer가 불변성을 자동으로 처리
},
// 할일 완료 상태 토글 액션
toggleTodo: (state, action: PayloadAction<number>) => {
const todo = state.todos.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
}
}
});
// 액션 생성자 내보내기
export const { addTodo, toggleTodo } = todoSlice.actions;
// 리듀서 내보내기
export default todoSlice.reducer;
// store.ts - Redux 스토어 설정
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from './todoSlice';
export const store = configureStore({
reducer: {
todos: todoReducer
}
});
// 스토어에서 RootState와 AppDispatch 타입 추출
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// hooks.ts - 타입 안전한 훅 생성
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// 타입화된 dispatch와 selector 훅
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// 컴포넌트에서 사용
import React, { useState } from 'react';
import { useAppDispatch, useAppSelector } from './hooks';
import { addTodo, toggleTodo } from './todoSlice';
const TodoApp: React.FC = () => {
const [text, setText] = useState('');
const dispatch = useAppDispatch();
// 스토어에서 상태 가져오기
const todos = useAppSelector(state => state.todos.todos);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (text.trim()) {
// 액션 디스패치
dispatch(addTodo(text));
setText('');
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="새 할 일 입력"
/>
<button type="submit">추가</button>
</form>
<ul>
{todos.map(todo => (
<li
key={todo.id}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => dispatch(toggleTodo(todo.id))}
>
{todo.text}
</li>
))}
</ul>
</div>
);
};
Redux와 Redux-Toolkit 사용 시 장점
- 예측 가능한 상태 변화: 단방향 데이터 흐름으로 상태 변화를 예측하기 쉽습니다.
- 디버깅 용이성: Redux DevTools를 통해 상태 변화를 추적하고 디버깅할 수 있습니다.
- 미들웨어 지원: 비동기 작업, 로깅 등을 위한 미들웨어를 쉽게 통합할 수 있습니다.
- 테스트 용이성: 순수 함수인 리듀서는 테스트하기 쉽습니다.
- 대규모 애플리케이션 지원: 복잡한 상태 관리가 필요한 대규모 애플리케이션에 적합합니다.
Redux-Toolkit vs Zustand
- Redux-Toolkit: 대규모 팀, 복잡한 상태 로직, 강력한 미들웨어가 필요한 경우 적합
- Zustand: 간결한 코드, 빠른 시작, 적은 보일러플레이트를 원하는 경우 적합
Redux-Toolkit은 기존 Redux의 복잡성을 크게 줄였지만, 여전히 Zustand보다는 설정이 많이 필요합니다. 하지만 대규모 애플리케이션에서는 Redux의 엄격한 패턴이 장기적으로 코드 유지보수에 도움이 될 수 있습니다.
리액트 상태 흐름 핵심 정리
단방향 데이터 흐름
- 리액트는 부모에서 자식으로 흐르는 단방향 데이터 흐름 원칙을 따름
- 데이터 흐름이 예측 가능하고 디버깅이 용이함
Props 드릴링
- 정의: 상위 컴포넌트에서 하위 컴포넌트로 props를 여러 계층을 거쳐 전달하는 패턴
- 유용한 경우: 단순한 계층 구조, 명확한 데이터 흐름, 소규모 앱
- 문제점: 코드 복잡성 증가, 유지보수 어려움, 불필요한 리렌더링
State Lifting (상태 끌어올리기)
- 정의: 하위 컴포넌트의 상태를 상위 컴포넌트로 끌어올려 관리하는 패턴
- 유용한 경우: 형제 컴포넌트 간 상태 공유, 단일 진실 공급원 유지
- 문제점: 상위 컴포넌트 비대화, 리렌더링 증가, 컴포넌트 재사용성 감소
문제 해결 방법
- 불필요한 리렌더링 방지
- React.memo, useCallback, useMemo 활용
- 상위 컴포넌트 비대화 해결
- 커스텀 훅으로 상태 로직 분리
- 컴포넌트 분해
상태 관리 기법
- Context API: 프롭스 드릴링 없이 컴포넌트 트리 전체에 데이터 제공
- Zustand: 간결한 API의 외부 상태 관리 라이브러리
- Redux Toolkit: 보일러플레이트 감소, 불변성 자동화
리액트의 Props 드릴링과 State Lifting은 기본적인 데이터 흐름 패턴이며, 이를 이해하는 것이 효과적인 상태 관리의 기초입니다. 앱이 복잡해질수록 이러한 패턴의 한계를 인식하고 적절한 보완책을 선택하는 것이 중요합니다.
React(리액트) 하위 컴포넌트에서 데이터(data) 받아서 상위 컴포넌트로 전달.
state. - 현재 컴포넌트에 렌더링에 영향을 미치는 객체 형태의 데이터 props. - 상위 컴포넌트에서 하위 컴포넌트로 객체 형태의 데이터를 전달 (읽기 전용) state 또는 props 업데이트 시 리렌더링된
covelope.tistory.com
https://ko.legacy.reactjs.org/docs/context.html
Context – React
A JavaScript library for building user interfaces
ko.legacy.reactjs.org
Redux Toolkit | Redux Toolkit
The official, opinionated, batteries-included toolset for efficient Redux development
redux-toolkit.js.org
Zustand
zustand-demo.pmnd.rs