Updated:

Redux

// 1. 액션 타입 정의
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
const SET_FILTER = 'SET_FILTER';

// 2. 액션 생성자
const addTodo = (text) => ({
  type: ADD_TODO,
  payload: {
    id: Date.now(),
    text,
    completed: false
  }
});

const toggleTodo = (id) => ({
  type: TOGGLE_TODO,
  payload: { id }
});

const deleteTodo = (id) => ({
  type: DELETE_TODO,
  payload: { id }
});

const setFilter = (filter) => ({
  type: SET_FILTER,
  payload: { filter }
});

// 3. 리듀서
const initialState = {
  todos: [],
  filter: 'all' // 'all', 'active', 'completed'
};

function todoReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, action.payload]
      };
      
    case TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
      
    case DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload.id)
      };
      
    case SET_FILTER:
      return {
        ...state,
        filter: action.payload.filter
      };
      
    default:
      return state;
  }
}

// 4. 스토어 생성
import { createStore } from 'redux';
const store = createStore(todoReducer);

// 5. React 컴포넌트와 연결
import { Provider, useSelector, useDispatch } from 'react-redux';

function TodoApp() {
  return (
    <Provider store={store}>
      <div className="todo-app">
        <h1>Todo List</h1>
        <AddTodoForm />
        <FilterButtons />
        <TodoList />
      </div>
    </Provider>
  );
}

function AddTodoForm() {
  const [text, setText] = useState('');
  const dispatch = useDispatch();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    dispatch(addTodo(text));
    setText('');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo"
      />
      <button type="submit">Add</button>
    </form>
  );
}

function FilterButtons() {
  const filter = useSelector(state => state.filter);
  const dispatch = useDispatch();
  
  return (
    <div className="filters">
      <button
        className={filter === 'all' ? 'active' : ''}
        onClick={() => dispatch(setFilter('all'))}
      >
        All
      </button>
      <button
        className={filter === 'active' ? 'active' : ''}
        onClick={() => dispatch(setFilter('active'))}
      >
        Active
      </button>
      <button
        className={filter === 'completed' ? 'active' : ''}
        onClick={() => dispatch(setFilter('completed'))}
      >
        Completed
      </button>
    </div>
  );
}

function TodoList() {
  const { todos, filter } = useSelector(state => state);
  const dispatch = useDispatch();
  
  // 필터에 따라 할 일 목록 필터링
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true; // 'all'
  });
  
  return (
    <ul className="todo-list">
      {filteredTodos.map(todo => (
        <li key={todo.id} className={todo.completed ? 'completed' : ''}>
          <span onClick={() => dispatch(toggleTodo(todo.id))}>
            {todo.text}
          </span>
          <button onClick={() => dispatch(deleteTodo(todo.id))}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

코멘트: Redux는 예측 가능한 상태 관리를 위한 인기 있는 라이브러리입니다. 주요 개념은 다음과 같습니다:

  1. 단일 스토어(Store): 애플리케이션의 전체 상태를 하나의 객체 트리에 저장
  2. 액션(Action): 상태 변경을 설명하는 일반 객체
  3. 리듀서(Reducer): 이전 상태와 액션을 받아 새 상태를 반환하는 순수 함수
  4. 디스패치(Dispatch): 액션을 스토어에 보내는 메서드

Redux는 다음과 같은 경우에 유용합니다:

  • 여러 컴포넌트에서 공유되는 복잡한 상태
  • 상태 변경 로직이 여러 곳에 분산된 경우
  • 시간 여행 디버깅, 상태 지속성 등의 고급 기능이 필요한 경우

Redux Toolkit

import { createSlice, configureStore } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';

// 1. 슬라이스 생성 (액션 + 리듀서)
const todoSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    filter: 'all' // 'all', 'active', 'completed'
  },
  reducers: {
    // 리듀서와 액션 생성자를 동시에 생성
    addTodo: (state, action) => {
      state.items.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      });
    },
    toggleTodo: (state, action) => {
      const todo = state.items.find(todo => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    deleteTodo: (state, action) => {
      state.items = state.items.filter(todo => todo.id !== action.payload);
    },
    setFilter: (state, action) => {
      state.filter = action.payload;
    }
  }
});

// 2. 액션 생성자 내보내기
export const { addTodo, toggleTodo, deleteTodo, setFilter } = todoSlice.actions;

// 3. 스토어 생성
const store = configureStore({
  reducer: todoSlice.reducer
});

// 4. React 컴포넌트
function TodoApp() {
  return (
    <Provider store={store}>
      <div className="todo-app">
        <h1>Todo List (Redux Toolkit)</h1>
        <AddTodoForm />
        <FilterButtons />
        <TodoList />
      </div>
    </Provider>
  );
}

function AddTodoForm() {
  const [text, setText] = useState('');
  const dispatch = useDispatch();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    dispatch(addTodo(text));
    setText('');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo"
      />
      <button type="submit">Add</button>
    </form>
  );
}

function FilterButtons() {
  const filter = useSelector(state => state.filter);
  const dispatch = useDispatch();
  
  return (
    <div className="filters">
      <button
        className={filter === 'all' ? 'active' : ''}
        onClick={() => dispatch(setFilter('all'))}
      >
        All
      </button>
      <button
        className={filter === 'active' ? 'active' : ''}
        onClick={() => dispatch(setFilter('active'))}
      >
        Active
      </button>
      <button
        className={filter === 'completed' ? 'active' : ''}
        onClick={() => dispatch(setFilter('completed'))}
      >
        Completed
      </button>
    </div>
  );
}

function TodoList() {
  const { items, filter } = useSelector(state => state);
  const dispatch = useDispatch();
  
  // 필터에 따라 할 일 목록 필터링
  const filteredTodos = items.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true; // 'all'
  });
  
  return (
    <ul className="todo-list">
      {filteredTodos.map(todo => (
        <li key={todo.id} className={todo.completed ? 'completed' : ''}>
          <span onClick={() => dispatch(toggleTodo(todo.id))}>
            {todo.text}
          </span>
          <button onClick={() => dispatch(deleteTodo(todo.id))}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

코멘트: Redux Toolkit은 Redux의 공식 권장 접근 방식으로, 보일러플레이트 코드를 줄이고 개발 경험을 개선합니다. 주요 기능은 다음과 같습니다:

  1. createSlice: 리듀서와 액션 생성자를 한 번에 생성
  2. Immer 통합: 불변성을 유지하면서도 “변경” 문법 사용 가능
  3. configureStore: 미들웨어와 개발자 도구 설정 간소화
  4. createAsyncThunk: 비동기 액션 처리 간소화

Redux Toolkit은 기존 Redux의 복잡성을 크게 줄이면서도 모든 이점을 유지합니다. 새 프로젝트에서는 일반 Redux보다 Redux Toolkit을 사용하는 것이 권장됩니다.

Zustand

import { create } from 'zustand';

// 1. 스토어 생성
const useTodoStore = create((set) => ({
  // 초기 상태
  todos: [],
  filter: 'all', // 'all', 'active', 'completed'
  
  // 액션
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, {
      id: Date.now(),
      text,
      completed: false
    }]
  })),
  
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    )
  })),
  
  deleteTodo: (id) => set((state) => ({
    todos: state.todos.filter(todo => todo.id !== id)
  })),
  
  setFilter: (filter) => set({ filter })
}));

// 2. React 컴포넌트
function TodoApp() {
  return (
    <div className="todo-app">
      <h1>Todo List (Zustand)</h1>
      <AddTodoForm />
      <FilterButtons />
      <TodoList />
    </div>
  );
}

function AddTodoForm() {
  const [text, setText] = useState('');
  const addTodo = useTodoStore((state) => state.addTodo);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    addTodo(text);
    setText('');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo"
      />
      <button type="submit">Add</button>
    </form>
  );
}

function FilterButtons() {
  const filter = useTodoStore((state) => state.filter);
  const setFilter = useTodoStore((state) => state.setFilter);
  
  return (
    <div className="filters">
      <button
        className={filter === 'all' ? 'active' : ''}
        onClick={() => setFilter('all')}
      >
        All
      </button>
      <button
        className={filter === 'active' ? 'active' : ''}
        onClick={() => setFilter('active')}
      >
        Active
      </button>
      <button
        className={filter === 'completed' ? 'active' : ''}
        onClick={() => setFilter('completed')}
      >
        Completed
      </button>
    </div>
  );
}

function TodoList() {
  // 필요한 상태만 선택적으로 구독
  const todos = useTodoStore((state) => state.todos);
  const filter = useTodoStore((state) => state.filter);
  const toggleTodo = useTodoStore((state) => state.toggleTodo);
  const deleteTodo = useTodoStore((state) => state.deleteTodo);
  
  // 필터에 따라 할 일 목록 필터링
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true; // 'all'
  });
  
  return (
    <ul className="todo-list">
      {filteredTodos.map(todo => (
        <li key={todo.id} className={todo.completed ? 'completed' : ''}>
          <span onClick={() => toggleTodo(todo.id)}>
            {todo.text}
          </span>
          <button onClick={() => deleteTodo(todo.id)}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

코멘트: Zustand는 간단하고 가벼운 상태 관리 라이브러리로, Redux의 복잡성 없이 유사한 기능을 제공합니다. 주요 특징은 다음과 같습니다:

  1. 간결한 API: 최소한의 보일러플레이트로 상태 관리
  2. Hook 기반: 컴포넌트에서 useTodoStore와 같은 Hook으로 직접 상태에 접근
  3. 선택적 구독: 필요한 상태만 선택적으로 구독하여 불필요한 리렌더링 방지
  4. 미들웨어 지원: Redux 개발자 도구, 지속성 등의 미들웨어 지원

Zustand는 작은 규모의 애플리케이션이나 Redux의 복잡성이 필요하지 않은 경우에 좋은 선택입니다. Context API보다 성능이 우수하면서도 사용이 간편합니다.

Jotai

import { atom, useAtom } from 'jotai';

// 1. 기본 아톰 정의
const todosAtom = atom([]);
const filterAtom = atom('all'); // 'all', 'active', 'completed'

// 2. 파생 아톰 (계산된 상태)
const filteredTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  const filter = get(filterAtom);
  
  if (filter === 'active') return todos.filter(todo => !todo.completed);
  if (filter === 'completed') return todos.filter(todo => todo.completed);
  return todos; // 'all'
});

// 3. 액션 아톰 (상태 업데이트 로직)
const addTodoAtom = atom(
  null, // 읽기 값 없음
  (get, set, text) => {
    const todos = get(todosAtom);
    set(todosAtom, [
      ...todos,
      {
        id: Date.now(),
        text,
        completed: false
      }
    ]);
  }
);

const toggleTodoAtom = atom(
  null,
  (get, set, id) => {
    const todos = get(todosAtom);
    set(todosAtom, todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }
);

const deleteTodoAtom = atom(
  null,
  (get, set, id) => {
    const todos = get(todosAtom);
    set(todosAtom, todos.filter(todo => todo.id !== id));
  }
);

// 4. React 컴포넌트
function TodoApp() {
  return (
    <div className="todo-app">
      <h1>Todo List (Jotai)</h1>
      <AddTodoForm />
      <FilterButtons />
      <TodoList />
    </div>
  );
}

function AddTodoForm() {
  const [text, setText] = useState('');
  const [, addTodo] = useAtom(addTodoAtom);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    addTodo(text);
    setText('');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo"
      />
      <button type="submit">Add</button>
    </form>
  );
}

function FilterButtons() {
  const [filter, setFilter] = useAtom(filterAtom);
  
  return (
    <div className="filters">
      <button
        className={filter === 'all' ? 'active' : ''}
        onClick={() => setFilter('all')}
      >
        All
      </button>
      <button
        className={filter === 'active' ? 'active' : ''}
        onClick={() => setFilter('active')}
      >
        Active
      </button>
      <button
        className={filter === 'completed' ? 'active' : ''}
        onClick={() => setFilter('completed')}
      >
        Completed
      </button>
    </div>
  );
}

function TodoList() {
  // 필터링된 할 일 목록 구독
  const [filteredTodos] = useAtom(filteredTodosAtom);
  const [, toggleTodo] = useAtom(toggleTodoAtom);
  const [, deleteTodo] = useAtom(deleteTodoAtom);
  
  return (
    <ul className="todo-list">
      {filteredTodos.map(todo => (
        <li key={todo.id} className={todo.completed ? 'completed' : ''}>
          <span onClick={() => toggleTodo(todo.id)}>
            {todo.text}
          </span>
          <button onClick={() => deleteTodo(todo.id)}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

코멘트: Jotai는 React를 위한 원자적(atomic) 상태 관리 라이브러리로, 상향식(bottom-up) 접근 방식을 취합니다. 주요 특징은 다음과 같습니다:

  1. 원자(Atom): 작은 상태 단위로, 다른 원자에서 파생될 수 있음
  2. 상향식 접근: 작은 상태 조각에서 시작하여 필요에 따라 조합
  3. React Suspense 지원: 비동기 상태 처리를 위한 내장 지원
  4. 최소한의 리렌더링: 사용된 원자가 변경될 때만 컴포넌트 리렌더링

Jotai는 특히 다음과 같은 경우에 적합합니다:

  • 전역 상태와 지역 상태 간의 경계가 모호한 경우
  • 상태 간의 의존성이 복잡한 경우
  • React Suspense와 함께 사용하는 경우
  • 리렌더링 최적화가 중요한 경우

Recoil

import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';

// 1. 아톰 정의 (기본 상태)
const todosAtom = atom({
  key: 'todosState', // 고유 키
  default: [] // 기본값
});

const filterAtom = atom({
  key: 'todoFilterState',
  default: 'all' // 'all', 'active', 'completed'
});

// 2. 선택자 정의 (파생 상태)
const filteredTodosSelector = selector({
  key: 'filteredTodosState',
  get: ({ get }) => {
    const todos = get(todosAtom);
    const filter = get(filterAtom);
    
    if (filter === 'active') return todos.filter(todo => !todo.completed);
    if (filter === 'completed') return todos.filter(todo => todo.completed);
    return todos; // 'all'
  }
});

const todoStatsSelector = selector({
  key: 'todoStatsState',
  get: ({ get }) => {
    const todos = get(todosAtom);
    const totalNum = todos.length;
    const completedNum = todos.filter(todo => todo.completed).length;
    const uncompletedNum = totalNum - completedNum;
    const percentCompleted = totalNum === 0 ? 0 : Math.round((completedNum / totalNum) * 100);
    
    return {
      totalNum,
      completedNum,
      uncompletedNum,
      percentCompleted
    };
  }
});

// 3. React 컴포넌트
function TodoApp() {
  return (
    <RecoilRoot>
      <div className="todo-app">
        <h1>Todo List (Recoil)</h1>
        <TodoStats />
        <AddTodoForm />
        <FilterButtons />
        <TodoList />
      </div>
    </RecoilRoot>
  );
}

function TodoStats() {
  const { totalNum, completedNum, uncompletedNum, percentCompleted } = useRecoilValue(todoStatsSelector);
  
  return (
    <div className="todo-stats">
      <p>Total: {totalNum}</p>
      <p>Completed: {completedNum}</p>
      <p>Uncompleted: {uncompletedNum}</p>
      <p>Percent Completed: {percentCompleted}%</p>
    </div>
  );
}

function AddTodoForm() {
  const [text, setText] = useState('');
  const setTodos = useSetRecoilState(todosAtom);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    
    setTodos(oldTodos => [
      ...oldTodos,
      {
        id: Date.now(),
        text,
        completed: false
      }
    ]);
    
    setText('');
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Add a new todo"
      />
      <button type="submit">Add</button>
    </form>
  );
}

function FilterButtons() {
  const [filter, setFilter] = useRecoilState(filterAtom);
  
  return (
    <div className="filters">
      <button
        className={filter === 'all' ? 'active' : ''}
        onClick={() => setFilter('all')}
      >
        All
      </button>
      <button
        className={filter === 'active' ? 'active' : ''}
        onClick={() => setFilter('active')}
      >
        Active
      </button>
      <button
        className={filter === 'completed' ? 'active' : ''}
        onClick={() => setFilter('completed')}
      >
        Completed
      </button>
    </div>
  );
}

function TodoList() {
  // 필터링된 할 일 목록 구독
  const filteredTodos = useRecoilValue(filteredTodosSelector);
  const setTodos = useSetRecoilState(todosAtom);
  
  const toggleTodo = (id) => {
    setTodos(oldTodos =>
      oldTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };
  
  const deleteTodo = (id) => {
    setTodos(oldTodos => oldTodos.filter(todo => todo.id !== id));
  };
  
  return (
    <ul className="todo-list">
      {filteredTodos.map(todo => (
        <li key={todo.id} className={todo.completed ? 'completed' : ''}>
          <span onClick={() => toggleTodo(todo.id)}>
            {todo.text}
          </span>
          <button onClick={() => deleteTodo(todo.id)}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

코멘트: Recoil은 Facebook에서 개발한 상태 관리 라이브러리로, React의 작동 방식과 잘 맞도록 설계되었습니다. 주요 특징은 다음과 같습니다:

  1. 아톰(Atom): 상태의 기본 단위, 컴포넌트가 구독할 수 있음
  2. 선택자(Selector): 아톰이나 다른 선택자에서 파생된 상태
  3. 비동기 지원: 비동기 데이터를 쉽게 처리할 수 있는 기능
  4. React Suspense 통합: 비동기 상태 로딩을 위한 내장 지원

Recoil은 특히 다음과 같은 경우에 적합합니다:

  • 복잡한 상태 의존성이 있는 경우
  • 비동기 상태 업데이트가 많은 경우
  • React의 Concurrent Mode와 함께 사용하는 경우
  • 상태 간의 관계를 명확하게 모델링해야 하는 경우

Categories:

Updated:

Leave a comment