400===Dev Library/React

React 실전 프로젝트 적용기: Todo 앱 만들기 📝

블로글러 2024. 10. 30. 23:02

오늘은 앞서 배운 React 핵심 개념들을 활용해서 실제 Todo 앱을 만들어보겠습니다!

1. 프로젝트 구조 설계 📂

src/
├── components/
│   ├── TodoInput.js
│   ├── TodoList.js
│   ├── TodoItem.js
│   └── TodoFilters.js
├── hooks/
│   └── useTodos.js
├── contexts/
│   └── TodoContext.js
└── App.js

2. 상태 관리 설계 (Context API) 🗃️

// TodoContext.js
const TodoContext = createContext();

export function TodoProvider({ children }) {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');

  const value = {
    todos,
    filter,
    addTodo: (text) => {
      setTodos([...todos, {
        id: Date.now(),
        text,
        completed: false
      }]);
    },
    toggleTodo: (id) => {
      setTodos(todos.map(todo => 
        todo.id === id 
          ? {...todo, completed: !todo.completed}
          : todo
      ));
    },
    // ... 다른 메서드들
  };

  return (
    <TodoContext.Provider value={value}>
      {children}
    </TodoContext.Provider>
  );
}

3. 커스텀 Hook 만들기 🎣

// useTodos.js
export function useTodos() {
  const [todos, setTodos] = useState([]);

  // 로컬 스토리지 연동
  useEffect(() => {
    const saved = localStorage.getItem('todos');
    if (saved) {
      setTodos(JSON.parse(saved));
    }
  }, []);

  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  return {
    todos,
    addTodo: (text) => {
      setTodos(prev => [...prev, { id: Date.now(), text, completed: false }]);
    },
    // ... 다른 메서드들
  };
}

4. 컴포넌트 구현하기 🎨

TodoInput 컴포넌트

function TodoInput() {
  const [text, setText] = useState('');
  const { addTodo } = useTodos();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    addTodo(text);
    setText('');
  };

  return (
    <form onSubmit={handleSubmit} className="todo-input">
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="할 일을 입력하세요"
      />
      <button type="submit">추가</button>
    </form>
  );
}

TodoItem 컴포넌트

const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>삭제</button>
    </li>
  );
});

5. 성능 최적화 적용 🚀

1. React.memo 사용

const TodoList = memo(function TodoList({ todos, onToggle }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
        />
      ))}
    </ul>
  );
});

2. useCallback으로 함수 최적화

const handleToggle = useCallback((id) => {
  setTodos(todos => 
    todos.map(todo =>
      todo.id === id 
        ? {...todo, completed: !todo.completed}
        : todo
    )
  );
}, []);

6. 스타일링 💅

.todo-app {
  max-width: 600px;
  margin: 2rem auto;
  padding: 2rem;
  box-shadow: 0 0 10px rgba(0,0,0,0.1);
}

.todo-input {
  display: flex;
  gap: 1rem;
  margin-bottom: 2rem;
}

.todo-item {
  display: flex;
  align-items: center;
  padding: 1rem;
  border-bottom: 1px solid #eee;
}

.todo-item.completed span {
  text-decoration: line-through;
  color: #888;
}

7. 애니메이션 효과 추가 ✨

import { motion, AnimatePresence } from 'framer-motion';

function TodoList({ todos }) {
  return (
    <ul>
      <AnimatePresence>
        {todos.map(todo => (
          <motion.li
            key={todo.id}
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, x: -100 }}
          >
            <TodoItem todo={todo} />
          </motion.li>
        ))}
      </AnimatePresence>
    </ul>
  );
}

8. 실제 사용 예시 📱

function App() {
  const { todos, addTodo, toggleTodo, deleteTodo } = useTodos();

  return (
    <div className="todo-app">
      <h1>할 일 관리</h1>
      <TodoInput onAdd={addTodo} />
      <TodoFilters />
      <TodoList
        todos={todos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
      />
      <div className="todo-stats">
        완료: {todos.filter(t => t.completed).length} / 전체: {todos.length}
      </div>
    </div>
  );
}

배운 점 & 주의사항 📌

  1. 상태 관리 전략

    • 작은 앱은 Context로 충분
    • 규모가 커지면 Redux나 Recoil 고려
  2. 성능 최적화

    • 필요한 곳에만 memo 사용
    • 불필요한 리렌더링 방지가 중요
  3. 사용자 경험

    • 로딩 상태 처리
    • 에러 처리
    • 애니메이션으로 부드러운 UX 제공

다음 단계 🚀

  1. 기능 확장

    • 카테고리 추가
    • 마감일 설정
    • 우선순위 지정
  2. 테스트 코드 작성

    test('Todo 추가 기능', () => {
      render(<TodoApp />);
      const input = screen.getByPlaceholderText('할 일을 입력하세요');
      fireEvent.change(input, { target: { value: '새로운 할 일' } });
      fireEvent.click(screen.getByText('추가'));
      expect(screen.getByText('새로운 할 일')).toBeInTheDocument();
    });

실제 프로젝트를 진행하면서 궁금한 점이 있으시다면 댓글로 남겨주세요! 😊

728x90