icon
3장 : React 컴포넌트 심화

간단한 할 일 목록 앱 만들기


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

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

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

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


실습 목표

  1. useState 훅을 이용한 상태 관리: 할 일 목록 배열과 입력 필드의 값을 관리합니다.
  2. 폼 다루기: 새로운 할 일을 입력받는 <input> 필드와 제출 버튼을 제어 컴포넌트로 구현합니다.
  3. 리스트 렌더링과 key: 할 일 목록을 map() 함수와 고유한 key를 사용하여 효율적으로 렌더링합니다.
  4. 이벤트 처리: 할 일 추가, 완료 상태 토글, 삭제 버튼 클릭 이벤트를 처리합니다.
  5. 조건부 렌더링: 할 일의 완료 상태에 따라 텍스트에 취소선을 긋는 등의 시각적 변화를 줍니다.
  6. (선택 사항) useEffect 활용: 초기 할 일 목록을 로드하거나, 할 일 목록이 변경될 때마다 로컬 스토리지에 저장하는 등의 부수 효과를 구현합니다.

프로젝트 준비

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

  1. src/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;
  2. src/components/TodoList.js 파일 생성 및 기본 구조 작성

    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 코드는 우리가 배운 여러 개념을 통합하여 사용하고 있습니다.

  1. useState를 이용한 상태 관리

    • todoItems: 현재 할 일 목록을 담는 배열 상태입니다. 각 할 일 객체는 id, text, completed 속성을 가집니다.
    • newTodo: 새로운 할 일을 입력받는 <input> 필드의 값을 관리하는 상태입니다.
  2. 폼 다루기 (제어 컴포넌트)

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

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

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

    • 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 상태와 연결하여 제어 컴포넌트로 만듭니다.
  6. (선택 사항) useEffect 활용 (로컬 스토리지)

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

실습 진행 방법

  1. 위의 App.jsTodoList.js 코드를 각각의 파일에 복사하여 붙여넣으세요.
  2. 개발 서버가 실행 중이지 않다면, 프로젝트 폴더에서 npm start (또는 yarn start) 명령어를 실행합니다.
  3. 브라우저를 열어 http://localhost:3000 (기본 포트)에 접속합니다.
  4. 새로운 할 일을 입력하고 '추가' 버튼을 클릭해보세요.
  5. 추가된 할 일 옆의 체크박스를 클릭하여 완료 상태를 토글해보고, '삭제' 버튼을 클릭하여 할 일을 제거해보세요.
  6. (선택 사항) 페이지를 새로고침했을 때 할 일 목록이 유지되는지 확인해 보세요.
  7. 개발자 도구(F12)의 'Console' 탭과 'Components' 탭을 활용하여 stateprops의 변화를 관찰해 보세요.

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