간단한 할 일 목록 앱 만들기
지금까지 우리는 리액트 컴포넌트의 심화 개념들을 깊이 있게 학습했습니다. 조건부 렌더링, 리스트와 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 // 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 // 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()
로 페이지 새로고침을 막고,newTodo
를todoItems
배열에 추가한 후newTodo
를 초기화합니다. 새로운 할 일에는id
를 부여하여 나중에key
로 사용합니다.
-
리스트 렌더링과
key
todoItems.map((item) => ...)
:todoItems
배열을 순회하며 각 할 일 아이템을<li>
JSX 요소로 변환합니다.key={item.id}
: 각<li>
요소에 할 일 객체의 고유id
를key
로 부여합니다. 이는 리액트가 목록을 효율적으로 업데이트하는 데 필수적입니다.
-
이벤트 처리
handleAddTodo
,handleToggleComplete
,handleDeleteTodo
함수들이 각각의 버튼 클릭이나 체크박스 변경 이벤트에 연결되어 있습니다.handleToggleComplete
,handleDeleteTodo
는id
를 인자로 받아 특정 할 일에 대한 작업을 수행합니다.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.js
와TodoList.js
코드를 각각의 파일에 복사하여 붙여넣으세요. - 개발 서버가 실행 중이지 않다면, 프로젝트 폴더에서
npm start
(또는yarn start
) 명령어를 실행합니다. - 브라우저를 열어
http://localhost:3000
(기본 포트)에 접속합니다. - 새로운 할 일을 입력하고 '추가' 버튼을 클릭해보세요.
- 추가된 할 일 옆의 체크박스를 클릭하여 완료 상태를 토글해보고, '삭제' 버튼을 클릭하여 할 일을 제거해보세요.
- (선택 사항) 페이지를 새로고침했을 때 할 일 목록이 유지되는지 확인해 보세요.
- 개발자 도구(F12)의 'Console' 탭과 'Components' 탭을 활용하여
state
와props
의 변화를 관찰해 보세요.
이제 여러분은 리액트의 핵심 개념들을 모두 통합하여 작지만 실제 동작하는 웹 애플리케이션의 한 부분을 직접 구현해 보셨습니다. 이 실습을 통해 조건부 렌더링, 리스트 렌더링, 폼 다루기, 그리고 useEffect
훅이 어떻게 상호작용하며 동적인 UI를 만드는지 명확하게 이해하셨기를 바랍니다.