누구나 로고
Online CoursesFor BusinessLibraryTrend InsightsFree coursesNoticesContactCareers
모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.
로그인무료 회원가입
강좌리액트 입문 — 컴포넌트로 만드는 웹19강

19강 / 전체 28강

리스트 앱 실전 ①

8분 읽기 조회 0

미니 프로젝트: 투두 앱을 처음부터 완성까지 만들면서 지금까지 배운 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
  • 누구나
이전 18강useReducer다음 20강 리스트 앱 실전 ②

댓글

0/1000

불러오는 중...

목차

  1. 1.리액트란 무엇인가
  2. 2.개발 환경 만들기 — Vite
  3. 3.JSX 기초
  4. 4.컴포넌트 만들기
  5. 5.Props로 데이터 전달
  6. 6.State와 useState
  7. 7.이벤트 처리
  8. 8.조건부 렌더링
  9. 9.리스트 렌더링과 key
  10. 10.폼 다루기
  11. 11.useEffect 기초
  12. 12.데이터 가져오기(fetch)
  13. 13.상태 끌어올리기
  14. 14.컴포넌트 합성과 재사용
  15. 15.useRef와 DOM 접근
  16. 16.커스텀 훅 만들기
  17. 17.Context API
  18. 18.useReducer
  19. 19.리스트 앱 실전 ①
  20. 20.리스트 앱 실전 ②
  21. 21.라우팅 — React Router
  22. 22.스타일링 방법들
  23. 23.리스트·상세 페이지 연결
  24. 24.비동기 상태 관리 패턴
  25. 25.성능 기초 — memo
  26. 26.에러 처리와 경계
  27. 27.환경변수와 빌드
  28. 28.배포하기

Support

  • Notices
  • FAQ
  • Contact Us
  • Community

Help & Info

  • Terms of Service
  • Privacy Policy
  • Refund Policy

Services

  • About Us
  • Sign up
  • New courses
  • Free courses
누구나 로고
Terms of ServicePrivacy PolicyRefund Policy

상호명: NUGUNA  |  대표자: 정우진  |  사업자등록번호: 392-32-01817  |  통신판매업신고: 제 2026-서울양천-0564 호

주소: 서울특별시 양천구 목동서로 100  |  이메일: nugunapass@gmail.com  |  전화: 010-6395-3043

© 2026 NUGUNA. All rights reserved.

KB예금주인증관리자
HomeCoursesLibraryContactMY