투두리스트 만들어보기
지금까지 배운 state, 참조값, useEffect를 합쳐서 localStorage 기반 투두리스트를 만듭니다.
배운 것을 합쳐봅시다
여기까지 오면 React의 핵심 도구를 한 바퀴 돌았습니다.
useState로 상태 관리- 참조값은 새로 만들어서 넘기기
useEffect로 화면 그린 다음에 할 일 처리
이번에는 이 셋을 합쳐서 투두리스트를 만들어 보겠습니다.
데이터는 localStorage에 저장해서, 브라우저를 새로고침해도 목록이 유지되게 할 겁니다.
완성 목표
- 입력창에 할 일을 적고 추가할 수 있다
- 각 항목을 삭제할 수 있다
- 브라우저를 닫았다 열어도 목록이 남아있다
1단계. 기본 화면 잡기
먼저 입력창과 목록만 있는 뼈대를 만듭니다.
import { useState } from 'react';
function App() {
const [todos, setTodos] = useState<string[]>([]);
const [input, setInput] = useState('');
return (
<div>
<h1>할 일 목록</h1>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="할 일을 입력하세요"
/>
<button>추가</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</div>
);
}state가 두 개입니다.
todos: 할 일 목록 (배열)input: 입력창에 쓰고 있는 텍스트
input의 onChange를 보면, 타이핑할 때마다 setInput으로 상태를 바꾸고 있습니다.
상태가 바뀌면 화면이 따라오니까, 입력창에 글자가 보이는 겁니다.
2단계. 항목 추가
버튼을 누르면 todos 배열에 항목을 추가합니다.
function addTodo() {
if (input.trim() === '') return;
setTodos([...todos, input]);
setInput('');
}[...todos, input] — 참조값 다루기에서 배운 패턴입니다.
기존 배열을 직접 바꾸지 않고, 새 배열을 만들어서 넘깁니다.
버튼에 연결합니다.
<button onClick={addTodo}>추가</button>Enter 키로도 추가할 수 있게 하면 더 편합니다.
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addTodo()}
placeholder="할 일을 입력하세요"
/>3단계. 항목 삭제
각 항목 옆에 삭제 버튼을 넣습니다.
function removeTodo(index: number) {
setTodos(todos.filter((_, i) => i !== index));
}filter로 해당 인덱스만 제외한 새 배열을 만듭니다.
이것도 참조값 다루기에서 본 패턴 그대로입니다.
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo}
<button onClick={() => removeTodo(index)}>삭제</button>
</li>
))}
</ul>여기까지 하면 추가/삭제가 되는 투두리스트가 완성됩니다.
하지만 새로고침하면 목록이 사라집니다.
4단계. localStorage에 저장하기
localStorage는 브라우저에 내장된 간단한 저장소입니다.
키-값 쌍으로 문자열을 저장하고, 브라우저를 닫았다 열어도 유지됩니다.
todos가 바뀔 때마다 저장하면 됩니다.
"상태가 바뀐 다음에 할 일" — useEffect가 딱 맞는 자리입니다.
import { useState, useEffect } from 'react';
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);todos가 바뀔 때마다 useEffect가 실행되면서 localStorage에 저장합니다.
배열은 문자열이 아니기 때문에 JSON.stringify로 변환해서 넣습니다.
5단계. localStorage에서 불러오기
화면이 처음 뜰 때 저장된 데이터를 불러와야 합니다.
여기서 useEffect를 쓸 수도 있지만, 더 좋은 방법이 있습니다.
useState에 함수를 넘기면, React가 첫 렌더링 때 딱 한 번만 실행해서 초기값을 정합니다.
const [todos, setTodos] = useState<string[]>(() => {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});이렇게 하면 처음부터 localStorage 데이터를 가지고 시작합니다.
불필요한 렌더링 없이 한 번에 끝납니다.
1단계에서 만들었던 useState<string[]>([]) 를 위 코드로 바꿔주면 됩니다.
전체 코드
import { useState, useEffect } from 'react';
function App() {
const [todos, setTodos] = useState<string[]>(() => {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
const [input, setInput] = useState('');
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
function addTodo() {
if (input.trim() === '') return;
setTodos([...todos, input]);
setInput('');
}
function removeTodo(index: number) {
setTodos(todos.filter((_, i) => i !== index));
}
return (
<div>
<h1>할 일 목록</h1>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addTodo()}
placeholder="할 일을 입력하세요"
/>
<button onClick={addTodo}>추가</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo}
<button onClick={() => removeTodo(index)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
export default App;코드가 길어 보이지만, 구조는 단순합니다.
- state 2개 (
todos,input) - useEffect 1개 (저장하기)
- 함수 2개 (
addTodo,removeTodo)
전부 이미 배운 것들의 조합입니다.
불러오기는 useState 초기값 함수에서, 저장하기는 useEffect에서 처리합니다.
직접 해보기
- 위 전체 코드를
src/App.tsx에 붙여넣고 실행해보기 - 할 일을 몇 개 추가한 뒤 새로고침해서 유지되는지 확인하기
- 완료 체크 기능을 추가해보기 (힌트:
string[]대신{ text: string, done: boolean }[])
다음 챕터 예고
리액트 기초 트랙의 마지막 번외 주제입니다.
JSX가 왜 등장했는지, 그 배경을 짧게 정리하고 마무리하겠습니다.