모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

19강 / 전체 30강

리스트 앱 실전 ①

8분 읽기 조회 12

미니 프로젝트: 투두 앱을 처음부터 완성까지 만들면서 지금까지 배운 useState, 이벤트, 리스트 렌더링, 조건부 렌더링을 실전에서 통합합니다.

🚀 미니 프로젝트: 투두(Todo) 앱 완성

이번 강의는 지금까지 배운 내용을 실제로 통합하는 미니 프로젝트입니다. 코드를 보기 전에 어떤 기능이 필요한지 먼저 정의합니다. 이 과정 자체가 실무 개발 방식과 동일합니다.

기능 목록 (기획 단계):

  • 할 일 추가 (입력창 + 버튼 or 엔터)
  • 할 일 완료 체크 (토글)
  • 할 일 삭제
  • 필터: 전체 / 할 일 / 완료
  • 남은 할 일 개수 표시
  • 전체 완료 시 메시지

🗂️ 타입 정의 및 상태 설계

코드를 시작하기 전에 데이터 구조를 먼저 정의합니다. TypeScript에서 타입을 먼저 설계하면 전체 구조가 명확해집니다:

interface Todo {
  id: number
  text: string
  completed: boolean
  createdAt: Date
}

type FilterType = 'all' | 'active' | 'completed'

// 상태: 투두 목록 + 필터
const [todos, setTodos] = useState<Todo[]>([])
const [filter, setFilter] = useState<FilterType>('all')
const [inputText, setInputText] = useState('')

💻 전체 구현

import { useState } from 'react'

interface Todo {
  id: number
  text: string
  completed: boolean
  createdAt: Date
}
type FilterType = 'all' | 'active' | 'completed'

export default function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([
    { id: 1, text: '리액트 공부하기', completed: false, createdAt: new Date() },
    { id: 2, text: '미니 프로젝트 완성', completed: false, createdAt: new Date() },
  ])
  const [filter, setFilter] = useState<FilterType>('all')
  const [inputText, setInputText] = useState('')

  // 추가
  function addTodo() {
    const text = inputText.trim()
    if (!text) return
    setTodos((prev) => [
      ...prev,
      { id: Date.now(), text, completed: false, createdAt: new Date() },
    ])
    setInputText('')
  }

  // 완료 토글
  function toggleTodo(id: number) {
    setTodos((prev) =>
      prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
    )
  }

  // 삭제
  function deleteTodo(id: number) {
    setTodos((prev) => prev.filter((t) => t.id !== id))
  }

  // 필터링
  const filteredTodos = todos.filter((t) => {
    if (filter === 'active') return !t.completed
    if (filter === 'completed') return t.completed
    return true
  })

  const activeCount = todos.filter((t) => !t.completed).length
  const allCompleted = todos.length > 0 && todos.every((t) => t.completed)

  return (
    <div className="min-h-screen bg-gray-50 flex items-start justify-center pt-16 px-4">
      <div className="w-full max-w-md">
        <h1 className="text-3xl font-black text-gray-900 mb-6">오늘 할 일 ✅</h1>

        {/* 입력창 */}
        <div className="flex gap-2 mb-4">
          <input
            value={inputText}
            onChange={(e) => setInputText(e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter') addTodo() }}
            placeholder="할 일을 입력하세요..."
            className="flex-1 border border-gray-200 rounded-xl px-4 py-3 text-sm outline-none focus:border-indigo-400"
          />
          <button
            onClick={addTodo}
            disabled={!inputText.trim()}
            className="px-5 py-3 bg-indigo-600 text-white rounded-xl text-sm font-bold hover:bg-indigo-700 disabled:opacity-40"
          >
            추가
          </button>
        </div>

        {/* 필터 탭 */}
        <div className="flex gap-1 mb-4">
          {(['all', 'active', 'completed'] as FilterType[]).map((f) => (
            <button
              key={f}
              onClick={() => setFilter(f)}
              className={`flex-1 py-2 rounded-lg text-sm font-medium ${
                filter === f
                  ? 'bg-indigo-600 text-white'
                  : 'bg-white border border-gray-200 text-gray-600 hover:bg-gray-50'
              }`}
            >
              {f === 'all' ? '전체' : f === 'active' ? '할 일' : '완료'}
            </button>
          ))}
        </div>

        {/* 목록 */}
        {allCompleted ? (
          <div className="bg-green-50 rounded-2xl p-8 text-center">
            <p className="text-2xl">🎉</p>
            <p className="text-green-700 font-bold mt-2">오늘 할 일을 모두 완료했습니다!</p>
          </div>
        ) : filteredTodos.length === 0 ? (
          <div className="bg-white rounded-2xl p-8 text-center text-gray-400 border border-gray-100">
            할 일이 없습니다
          </div>
        ) : (
          <ul className="space-y-2">
            {filteredTodos.map((todo) => (
              <li
                key={todo.id}
                className="bg-white rounded-xl border border-gray-100 px-4 py-3 flex items-center gap-3 group"
              >
                <button
                  onClick={() => toggleTodo(todo.id)}
                  className={`w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 ${
                    todo.completed
                      ? 'bg-green-500 border-green-500'
                      : 'border-gray-300 hover:border-indigo-400'
                  }`}
                >
                  {todo.completed && <span className="text-white text-xs">✓</span>}
                </button>
                <span
                  className={`flex-1 text-sm ${
                    todo.completed ? 'line-through text-gray-400' : 'text-gray-800'
                  }`}
                >
                  {todo.text}
                </span>
                <button
                  onClick={() => deleteTodo(todo.id)}
                  className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 transition-opacity text-xs"
                >
                  삭제
                </button>
              </li>
            ))}
          </ul>
        )}

        {/* 남은 할 일 카운트 */}
        {todos.length > 0 && (
          <p className="text-center text-xs text-gray-400 mt-4">
            남은 할 일: {activeCount}개
          </p>
        )}
      </div>
    </div>
  )
}

🧩 코드 포인트 정리

이 프로젝트에서 적용한 개념들:

  • useState: todos 배열, filter, inputText 세 가지 상태 관리
  • 불변성: map으로 새 배열 생성(토글), filter로 새 배열 생성(삭제), 스프레드로 추가
  • 조건부 렌더링: 전체 완료 → 축하 메시지, 빈 목록 → 안내, 아니면 목록
  • 리스트 렌더링: map + key={todo.id}
  • 이벤트: onClick, onChange, onKeyDown, 그룹 hover로 삭제 버튼 표시

✅ 정리

  • 타입 먼저 정의 → 상태 설계 → UI 구현 순서로 진행했습니다.
  • 불변성 원칙: 추가는 스프레드, 수정은 map, 삭제는 filter.
  • group / group-hover 클래스로 마우스 오버 시에만 삭제 버튼 표시.
  • 다음 강의에서 이 앱에 라우팅을 추가해봅니다.

관련 주제

  • Todo 앱 요구사항 정의
  • 컴포넌트 설계
  • 입력 추가 기능
  • 리스트 렌더링 실습
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 리액트 입문 — 컴포넌트로 만드는 웹
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

리액트 입문 — 컴포넌트로 만드는 웹

이 강의가 속한 시리즈는 총 30강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...