icon
3장 : React 컴포넌트 심화

간단한 할 일 목록 앱 만들기


지금까지 우리는 리액트 컴포넌트의 심화 개념들을 깊이 있게 학습했습니다. 조건부 렌더링, 리스트와 key 사용, 폼 다루기, 그리고 useEffect 훅까지, 이제 이 모든 지식을 활용하여 작지만 기능적인 애플리케이션을 직접 만들어 볼 시간입니다.

이번 실습에서는 간단한 "할 일 목록(Todo List)" 애플리케이션을 만들어 볼 것입니다. 이 앱은 다음과 같은 기능을 가질 것입니다.

  • 새로운 할 일 추가
  • 할 일 목록 렌더링
  • 각 할 일 완료 상태 토글 (체크박스)
  • 할 일 삭제

이 실습을 통해 배운 개념들이 실제 코드에서 어떻게 유기적으로 동작하는지 체감하고, 리액트 개발에 대한 자신감을 얻으실 수 있기를 바랍니다.


실습 목표

useState 훅을 이용한 상태 관리: 할 일 목록 배열과 입력 필드의 값을 관리합니다.

폼 다루기: 새로운 할 일을 입력받는 <input> 필드와 제출 버튼을 제어 컴포넌트로 구현합니다.

리스트 렌더링과 key: 할 일 목록을 map() 함수와 고유한 key를 사용하여 효율적으로 렌더링합니다.

이벤트 처리: 할 일 추가, 완료 상태 토글, 삭제 버튼 클릭 이벤트를 처리합니다.

조건부 렌더링: 할 일의 완료 상태에 따라 텍스트에 취소선을 긋는 등의 시각적 변화를 줍니다.

(선택 사항) useEffect 활용: 초기 할 일 목록을 로드하거나, 할 일 목록이 변경될 때마다 로컬 스토리지에 저장하는 등의 부수 효과를 구현합니다.


프로젝트 준비

create-react-app으로 생성된 프로젝트가 있다고 가정합니다. src 폴더에 components 폴더를 만들고, 그 안에 TodoList.js 파일을 만들고 App.js에서 이를 렌더링하는 방식으로 진행하겠습니다.

src/App.js 수정 (기존 내용 대체)

src/App.js
import React from 'react';
import './App.css'; // 필요하다면 CSS 파일을 유지합니다.
import TodoList from './components/TodoList'; // TodoList 컴포넌트 불러오기

function App() {
  return (
    <div className="App">
      <h1>나의 멋진 Todo List</h1>
      <TodoList />
    </div>
  );
}

export default App;

src/components/TodoList.js 파일 생성 및 기본 구조 작성

src/components/TodoList.js
import React, { useState, useEffect } from 'react';

function TodoList() {
  // 1. 상태 정의: 할 일 목록 배열 (todoItems)과 새 할 일 입력 값 (newTodo)
  const [todoItems, setTodoItems] = useState(() => {
    // (선택 사항) 로컬 스토리지에서 초기 할 일 목록 불러오기
    const savedTodos = localStorage.getItem('todoItems');
    return savedTodos ? JSON.parse(savedTodos) : [];
  });
  const [newTodo, setNewTodo] = useState('');

  // 2. (선택 사항) todoItems가 변경될 때마다 로컬 스토리지에 저장
  useEffect(() => {
    localStorage.setItem('todoItems', JSON.stringify(todoItems));
  }, [todoItems]); // todoItems가 의존성 배열에 있으므로, 변경될 때마다 실행

  // 3. 새 할 일 입력 필드 변경 핸들러
  const handleInputChange = (e) => {
    setNewTodo(e.target.value);
  };

  // 4. 할 일 추가 핸들러
  const handleAddTodo = (e) => {
    e.preventDefault(); // 폼 제출 시 페이지 새로고침 방지
    if (newTodo.trim() === '') return; // 빈 값은 추가하지 않음

    const newId = todoItems.length > 0 ? Math.max(...todoItems.map(item => item.id)) + 1 : 1;
    const newTodoItem = {
      id: newId,
      text: newTodo,
      completed: false, // 초기에는 완료되지 않은 상태
    };

    setTodoItems([...todoItems, newTodoItem]); // 기존 목록에 새 할 일 추가
    setNewTodo(''); // 입력 필드 초기화
  };

  // 5. 할 일 완료 상태 토글 핸들러
  const handleToggleComplete = (id) => {
    setTodoItems(
      todoItems.map((item) =>
        item.id === id ? { ...item, completed: !item.completed } : item
      )
    );
  };

  // 6. 할 일 삭제 핸들러
  const handleDeleteTodo = (id) => {
    setTodoItems(todoItems.filter((item) => item.id !== id));
  };

  // 7. JSX 렌더링
  return (
    <div style={{ maxWidth: '500px', margin: '30px auto', padding: '20px', border: '1px solid #eee', borderRadius: '10px', boxShadow: '0 2px 10px rgba(0,0,0,0.1)' }}>
      {/* 할 일 추가 폼 */}
      <form onSubmit={handleAddTodo} style={{ display: 'flex', marginBottom: '20px' }}>
        <input
          type="text"
          value={newTodo}
          onChange={handleInputChange}
          placeholder="새 할 일을 입력하세요..."
          style={{ flexGrow: 1, padding: '10px', fontSize: '16px', border: '1px solid #ddd', borderRadius: '5px 0 0 5px' }}
        />
        <button type="submit" style={{ padding: '10px 15px', fontSize: '16px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '0 5px 5px 0', cursor: 'pointer' }}>
          추가
        </button>
      </form>

      {/* 할 일 목록 */}
      {todoItems.length === 0 ? (
        <p style={{ textAlign: 'center', color: '#666' }}>아직 할 일이 없습니다. 새 할 일을 추가해 보세요!</p>
      ) : (
        <ul style={{ listStyle: 'none', padding: 0 }}>
          {todoItems.map((item) => (
            <li
              key={item.id} // ⭐️ key 속성 중요!
              style={{
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'space-between',
                padding: '10px',
                borderBottom: '1px dashed #eee',
                backgroundColor: item.completed ? '#e6ffe6' : 'white', // 조건부 스타일링
              }}
            >
              <div style={{ display: 'flex', alignItems: 'center', flexGrow: 1 }}>
                <input
                  type="checkbox"
                  checked={item.completed} // ⭐️ checked 속성으로 state와 연결
                  onChange={() => handleToggleComplete(item.id)}
                  style={{ marginRight: '10px', transform: 'scale(1.2)' }}
                />
                <span
                  style={{
                    fontSize: '18px',
                    textDecoration: item.completed ? 'line-through' : 'none', // 조건부 렌더링 (취소선)
                    color: item.completed ? '#999' : '#333',
                  }}
                >
                  {item.text}
                </span>
              </div>
              <button
                onClick={() => handleDeleteTodo(item.id)}
                style={{
                  backgroundColor: '#dc3545',
                  color: 'white',
                  border: 'none',
                  padding: '5px 10px',
                  borderRadius: '5px',
                  cursor: 'pointer',
                  fontSize: '14px',
                }}
              >
                삭제
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default TodoList;

코드 설명 및 핵심 개념 복습

TodoList.js 코드는 우리가 배운 여러 개념을 통합하여 사용하고 있습니다.

useState를 이용한 상태 관리

  • todoItems: 현재 할 일 목록을 담는 배열 상태입니다. 각 할 일 객체는 id, text, completed 속성을 가집니다.
  • newTodo: 새로운 할 일을 입력받는 <input> 필드의 값을 관리하는 상태입니다.

폼 다루기 (제어 컴포넌트)

  • handleInputChange: <input> 필드의 onChange 이벤트에 연결되어 newTodo 상태를 실시간으로 업데이트합니다. value={newTodo}를 통해 input 필드가 newTodo 상태에 의해 제어됩니다.
  • handleAddTodo: <form>onSubmit 이벤트에 연결됩니다. e.preventDefault()로 페이지 새로고침을 막고, newTodotodoItems 배열에 추가한 후 newTodo를 초기화합니다. 새로운 할 일에는 id를 부여하여 나중에 key로 사용합니다.

리스트 렌더링과 key

  • todoItems.map((item) => ...): todoItems 배열을 순회하며 각 할 일 아이템을 <li> JSX 요소로 변환합니다.
  • key={item.id}: 각 <li> 요소에 할 일 객체의 고유 idkey로 부여합니다. 이는 리액트가 목록을 효율적으로 업데이트하는 데 필수적입니다.

이벤트 처리

  • handleAddTodo, handleToggleComplete, handleDeleteTodo 함수들이 각각의 버튼 클릭이나 체크박스 변경 이벤트에 연결되어 있습니다.
  • handleToggleComplete, handleDeleteTodoid를 인자로 받아 특정 할 일에 대한 작업을 수행합니다. map() 함수 내부에서 onClick={() => handleDeleteTodo(item.id)}와 같이 화살표 함수로 감싸 인자를 전달하는 방식이 사용되었습니다.

조건부 렌더링

  • todoItems.length === 0 ? <p> ... </p> : <ul> ... </ul>: todoItems가 비어있으면 "할 일이 없습니다" 메시지를, 그렇지 않으면 목록을 렌더링합니다. (삼항 연산자)
  • backgroundColor: item.completed ? '#e6ffe6' : 'white': 할 일이 완료되면 배경색을 변경합니다.
  • textDecoration: item.completed ? 'line-through' : 'none': 할 일이 완료되면 텍스트에 취소선을 적용합니다. (삼항 연산자)
  • checked={item.completed}: 체크박스의 checked 속성을 item.completed 상태와 연결하여 제어 컴포넌트로 만듭니다.

(선택 사항) useEffect 활용 (로컬 스토리지)

  • 첫 번째 useState의 초기값 설정 부분에서 localStorage.getItem('todoItems')를 통해 이전에 저장된 할 일 목록이 있다면 불러와 todoItems의 초기값으로 사용합니다.
  • 두 번째 useEffect 훅은 todoItems 상태가 변경될 때마다 ([todoItems] 의존성 배열 때문에) localStorage.setItem을 사용하여 현재의 todoItems 배열을 로컬 스토리지에 JSON 문자열 형태로 저장합니다. 이렇게 하면 페이지를 새로고침해도 할 일 목록이 유지됩니다.

실습 진행 방법

위의 App.jsTodoList.js 코드를 각각의 파일에 복사하여 붙여넣으세요.

개발 서버가 실행 중이지 않다면, 프로젝트 폴더에서 npm start (또는 yarn start) 명령어를 실행합니다.

브라우저를 열어 http://localhost:3000 (기본 포트)에 접속합니다.

새로운 할 일을 입력하고 '추가' 버튼을 클릭해보세요.

추가된 할 일 옆의 체크박스를 클릭하여 완료 상태를 토글해보고, '삭제' 버튼을 클릭하여 할 일을 제거해보세요.

(선택 사항) 페이지를 새로고침했을 때 할 일 목록이 유지되는지 확인해 보세요.

개발자 도구(F12)의 'Console' 탭과 'Components' 탭을 활용하여 stateprops의 변화를 관찰해 보세요.


이제 여러분은 리액트의 핵심 개념들을 모두 통합하여 작지만 실제 동작하는 웹 애플리케이션의 한 부분을 직접 구현해 보셨습니다. 이 실습을 통해 조건부 렌더링, 리스트 렌더링, 폼 다루기, 그리고 useEffect 훅이 어떻게 상호작용하며 동적인 UI를 만드는지 명확하게 이해하셨기를 바랍니다.