간단한 할 일 목록 앱 만들기
할 일의 추가·완료 전환·삭제 상태를 설계하고 폼·리스트·key·조건부 렌더링을 결합해 Todo 앱을 완성합니다.
지금까지 조건부 렌더링, 리스트와 key, 폼, useEffect까지 React 컴포넌트의 심화 개념을 익혔습니다. 이제 이 지식을 활용해 작지만 제대로 동작하는 애플리케이션을 직접 만들어 볼 차례입니다. 이 글에서는 간단한 할 일 목록(Todo List) 애플리케이션을 만듭니다. 이 앱은 새 할 일을 추가하고, 할 일 목록을 보여 주며, 체크박스로 각 할 일의 완료 상태를 바꾸고, 할 일을 삭제할 수 있습니다. 실습을 통해 배운 개념이 실제 코드에서 어떻게 맞물리는지 체감해 보세요.
실습 목표: Todo 상태 흐름 안정화
이 실습은 완성된 예제를 따라가며 입력과 목록의 상태가 어떻게 바뀌는지 확인하는 방식으로 진행합니다. 특히 key를 어떻게 고르고 상태를 어떻게 갱신하느냐가 UI의 일관성에 어떤 영향을 주는지 점검합니다.
이 앱에는 앞에서 배운 개념이 모두 들어 있습니다. useState로 할 일 목록 배열과 입력 필드의 값을 관리하고, 새 할 일을 받는 <input>과 제출 버튼을 제어 컴포넌트로 만듭니다. 할 일 목록은 map()과 고유한 key로 렌더링하고, 할 일의 추가와 완료 상태 전환, 삭제를 이벤트로 처리하며, 완료 상태에 따라 글자에 취소선을 긋는 조건부 렌더링을 씁니다. 선택 사항으로, 할 일 목록이 바뀔 때마다 useEffect로 로컬 스토리지에 저장해 새로고침해도 목록이 남게 합니다.
준비 단계: 상태 모델 설계
먼저 어떤 값을 어떤 state로 둘지 정합니다. 화면 안에서만 쓰는 단순한 값은 useState로 충분하지만, 저장과 복원의 규칙이 늘어나기 시작하면 상태의 흐름을 분명히 나눠 관리하는 편이 안전합니다. 여기서는 할 일 목록 배열 todoItems와 입력 중인 글자 newTodo, 두 개의 state를 씁니다.
Vite로 만든 프로젝트가 있다고 가정하고, src 폴더에 components 폴더를 만든 뒤 그 안에 TodoList.jsx를 만들어 App.jsx에서 렌더링하는 방식으로 진행합니다. 로컬 스토리지 부분은 저장소에 접근할 수 있고 저장된 JSON이 올바른 Todo 배열이라는 조건을 전제로 하며, 손상된 데이터의 복구나 형식 검증은 구현하지 않습니다. 먼저 src/App.jsx의 기존 내용을 다음으로 바꿉니다.
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;App은 제목과 TodoList 하나만 그리고, 상태와 이벤트 처리는 모두 TodoList가 맡습니다. 그다음 src/components/TodoList.jsx 파일을 만들고 다음 코드를 작성합니다. 아래 코드는 주석의 번호 1.부터 7.까지 상태 정의, 저장, 입력, 추가, 완료 전환, 삭제, 렌더링의 순서로 읽으면 되며, 각 부분이 하는 일은 코드 뒤에서 하나씩 짚습니다.
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}
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}
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.jsx 코드는 지금까지 배운 여러 개념을 함께 씁니다. 먼저 state입니다. todoItems는 현재 할 일 목록을 담는 배열 state로, 각 할 일 객체는 id, text, completed 속성을 가집니다. newTodo는 새 할 일을 받는 <input> 필드의 값을 관리하는 state입니다.
폼은 제어 컴포넌트로 다룹니다. handleInputChange는 <input>의 onChange에 연결되어 newTodo를 실시간으로 갱신하고, value={newTodo}로 입력 필드가 newTodo에 따라 제어됩니다. handleAddTodo는 <form>의 onSubmit에 연결되어, e.preventDefault()로 페이지 새로고침을 막고 newTodo를 todoItems 배열에 더한 뒤 newTodo를 비웁니다. 새 할 일에는 나중에 key로 쓸 id를 주는데, 현재 목록의 가장 큰 id에 1을 더하고 목록이 비어 있으면 1을 씁니다.
리스트는 todoItems.map((item) => ...)으로 각 할 일을 <li> 요소로 바꿔 렌더링하며, 각 <li>에 key={item.id}로 할 일의 고유 id를 줍니다. 이벤트는 handleAddTodo, handleToggleComplete, handleDeleteTodo가 각각 제출과 체크박스 변경, 삭제 버튼 클릭에 연결되어 처리합니다. 이 가운데 handleToggleComplete와 handleDeleteTodo는 id를 인자로 받아 특정 할 일에 대한 작업을 하며, map() 안에서 onClick={() => handleDeleteTodo(item.id)}처럼 화살표 함수로 감싸 인자를 넘깁니다.
조건부 렌더링은 여러 곳에서 씁니다. todoItems.length === 0 ? <p> ... </p> : <ul> ... </ul>은 목록이 비어 있으면 “아직 할 일이 없습니다. 새 할 일을 추가해 보세요!”를, 그렇지 않으면 목록을 보여 주는 삼항 연산자입니다. 할 일이 완료되면 backgroundColor: item.completed ? '#e6ffe6' : 'white'로 배경색이 바뀌고, textDecoration: item.completed ? 'line-through' : 'none'으로 글자에 취소선이 그어집니다. 체크박스의 checked 속성도 item.completed와 연결해 제어 컴포넌트로 만들었습니다.
마지막으로 로컬 스토리지입니다. 저장된 목록을 불러오는 일은 useEffect가 아니라 첫 번째 useState에 넘긴 초기화 함수가 맡습니다. 이 함수가 localStorage.getItem('todoItems')로 이전에 저장한 목록을 읽어, 있다면 todoItems의 초기값으로 씁니다. 그리고 useEffect가 의존성 배열 [todoItems]에 따라 목록이 바뀔 때마다 localStorage.setItem으로 현재의 todoItems를 JSON 문자열로 저장하므로, 페이지를 새로고침해도 할 일 목록이 남습니다.
검증 순서: 추가·토글·삭제
Todo 상태 전이 — 상태 추적
추가/토글/삭제에서 목록/초안/항목 참조/저장 값이 바뀐다. 실제 앱의 독립 동작과 검증 시나리오 순서를 구분하고 손상 JSON 복구는 구현된 것으로 쓰지 않는다.
TODO: THREE OPERATIONS
추가·토글·삭제는 같은 목록에 적용하는 독립적인 변경입니다
newTodo는 입력 초안, todoItems는 저장할 목록입니다. 각 동작은 새 배열을 만들고 React가 다음 화면을 표시합니다. 검증 순서와 사용자가 선택하는 동작을 구분합니다.
동일한 목록에서 고르는 독립 동작
추가→토글→삭제는 실습 검증 순서입니다. 실제 앱에서는 세 동작을 독립적으로 선택합니다. trim 거부·원 text·id·유효 저장값의 전제는 아래 원설명에 보존합니다.
원래 설명과 상세 기준
추가 · 새 항목 붙이기
전: [] / newTodo: "복습"
후: [{id:1, text:"복습",
completed:false}]
입력 초안: ""공백뿐인 입력은 trim으로 거부하지만 저장할 text는 원래 newTodo입니다. ID는 현재 최대값+1, 빈 목록이면 1입니다.
토글 · 대상만 새 객체
대상 id: 1
전: completed:false
후: completed:true
나머지 항목: 그대로map으로 같은 id의 객체만 복사해 completed를 반전합니다. checked·취소선·배경이 같은 상태를 읽습니다.
삭제 · 대상만 제외
전: [id1, id2]
삭제 대상: id1
후: [id2]filter로 대상 id를 제외합니다. id2의 key는 유지되며, 삭제가 새 입력을 만들거나 완료 토글을 요구하지는 않습니다.
다음 렌더와 저장
todoItems → map / key={item.id}
목록 변경 → [todoItems] Effect
→ localStorage JSON초기 state는 저장값을 읽습니다. 유효한 Todo 배열과 저장소 접근이 가능한 환경을 전제로 하며 손상 JSON 복구는 본문에 없습니다.
실습 검증 · 사용자 동작과 구별
추가→토글→삭제→새로고침은 테스트 순서입니다. 실제 앱은 토글과 삭제를 각각 선택합니다. 모든 항목을 지운 뒤 새 항목의 ID가 다시 1이 될 수 있어도 현재 형제 목록 안에서는 고유합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 설명 | 공백뿐인 입력은 trim으로 거부하지만 저장할 text는 원래 newTodo입니다. ID는 현재 최댓값+1, 빈 목록이면 1입니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 추가 | 목록: id1 복습 false / 초안: 빈 문자열 | 빈 목록과 복습 초안에서 id1 항목을 만든 뒤 초안을 비운다 | trim 거절 조건과 저장하는 원 text를 구분한다 |
| 토글 | id1: true / 다른 항목: 참조/값 유지 | id1만 새 객체로 completed:true가 된다 | map이 대상 객체만 바꾼다 |
| 삭제와 저장 | 삭제: 대상 제외 / 저장: todoItems JSON | id1 삭제 후 남은 목록을 다음 렌더에 반영하고 Effect로 저장한다 | 순서는 검증 시나리오이며 실제 동작은 독립 선택이고 손상 JSON 복구는 없다 |
Todo 상태 전이 — 상태 추적
추가/토글/삭제에서 목록/초안/항목 참조/저장 값이 바뀐다. 실제 앱의 독립 동작과 검증 시나리오 순서를 구분하고 손상 JSON 복구는 구현된 것으로 쓰지 않는다.
TODO: THREE OPERATIONS
추가·토글·삭제는 같은 목록에 적용하는 독립적인 변경입니다
newTodo는 입력 초안, todoItems는 저장할 목록입니다. 각 동작은 새 배열을 만들고 React가 다음 화면을 표시합니다. 검증 순서와 사용자가 선택하는 동작을 구분합니다.
동일한 목록에서 고르는 독립 동작
추가→토글→삭제는 실습 검증 순서입니다. 실제 앱에서는 세 동작을 독립적으로 선택합니다. trim 거부·원 text·id·유효 저장값의 전제는 아래 원설명에 보존합니다.
원래 설명과 상세 기준
추가 · 새 항목 붙이기
전: [] / newTodo: "복습"
후: [{id:1, text:"복습",
completed:false}]
입력 초안: ""공백뿐인 입력은 trim으로 거부하지만 저장할 text는 원래 newTodo입니다. ID는 현재 최대값+1, 빈 목록이면 1입니다.
토글 · 대상만 새 객체
대상 id: 1
전: completed:false
후: completed:true
나머지 항목: 그대로map으로 같은 id의 객체만 복사해 completed를 반전합니다. checked·취소선·배경이 같은 상태를 읽습니다.
삭제 · 대상만 제외
전: [id1, id2]
삭제 대상: id1
후: [id2]filter로 대상 id를 제외합니다. id2의 key는 유지되며, 삭제가 새 입력을 만들거나 완료 토글을 요구하지는 않습니다.
다음 렌더와 저장
todoItems → map / key={item.id}
목록 변경 → [todoItems] Effect
→ localStorage JSON초기 state는 저장값을 읽습니다. 유효한 Todo 배열과 저장소 접근이 가능한 환경을 전제로 하며 손상 JSON 복구는 본문에 없습니다.
실습 검증 · 사용자 동작과 구별
추가→토글→삭제→새로고침은 테스트 순서입니다. 실제 앱은 토글과 삭제를 각각 선택합니다. 모든 항목을 지운 뒤 새 항목의 ID가 다시 1이 될 수 있어도 현재 형제 목록 안에서는 고유합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 설명 | 공백뿐인 입력은 trim으로 거부하지만 저장할 text는 원래 newTodo입니다. ID는 현재 최댓값+1, 빈 목록이면 1입니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 추가 | 목록: id1 복습 false / 초안: 빈 문자열 | 빈 목록과 복습 초안에서 id1 항목을 만든 뒤 초안을 비운다 | trim 거절 조건과 저장하는 원 text를 구분한다 |
| 토글 | id1: true / 다른 항목: 참조/값 유지 | id1만 새 객체로 completed:true가 된다 | map이 대상 객체만 바꾼다 |
| 삭제와 저장 | 삭제: 대상 제외 / 저장: todoItems JSON | id1 삭제 후 남은 목록을 다음 렌더에 반영하고 Effect로 저장한다 | 순서는 검증 시나리오이며 실제 동작은 독립 선택이고 손상 JSON 복구는 없다 |
위 그림은 추가, 토글, 삭제가 같은 todoItems 목록에 적용하는 서로 독립된 변경이라는 점을 보여 줍니다. newTodo는 입력 중인 초안이고 todoItems는 저장할 목록이며, 세 동작은 모두 새 배열을 만들어 React가 다음 화면을 그리게 합니다. 추가는 []인 목록에 newTodo가 “복습”일 때 [{id:1, text:"복습", completed:false}]를 만들고 입력 초안을 비웁니다. 공백뿐인 입력은 trim으로 걸러 내지만, 저장하는 text는 원래의 newTodo입니다.
토글은 map으로 같은 id의 객체만 복사해 completed를 뒤집고 나머지 항목은 그대로 두며, 체크박스의 checked와 취소선, 배경색이 모두 같은 상태를 읽습니다. 삭제는 [id1, id2]에서 filter로 대상 id1을 빼 [id2]를 만들며, 남은 id2의 key는 그대로입니다. 목록이 바뀌면 다음 렌더에서 key={item.id}로 목록을 그리고, [todoItems] Effect가 로컬 스토리지에 JSON으로 저장하며, 새로고침하면 초기 state가 그 저장값을 읽습니다.
그림의 설명처럼 “추가 → 토글 → 삭제 → 새로고침”은 실습에서 확인하는 순서일 뿐, 실제 앱의 사용자는 세 동작을 각각 골라 씁니다. 또 모든 항목을 지운 뒤 새 항목을 추가하면 ID가 다시 1이 될 수 있지만, 현재 형제 목록 안에서는 여전히 고유합니다.
실습은 이 순서대로 확인합니다. 새로고침까지 확인하려면 삭제한 뒤에도 남는 항목이 있어야 합니다. 모든 항목을 지워 빈 목록만 확인하면, 저장된 항목이 다음 로딩에서 되살아나는지 구별하기 어렵기 때문입니다. 시작 조건으로는 다른 실습 데이터가 섞이지 않은 별도의 연습 프로젝트에서, 로컬 스토리지의 todoItems 키가 비어 있어야 합니다. 기존 데이터가 있다면 먼저 따로 보관하고, 처음 목록이 비어 있는지 확인하세요.
| 동작 | 다음 목록과 확인할 표시 |
|---|---|
| 복습 추가 | ID 1 복습, 미완료. 입력칸은 비어 있음 |
| 예제 추가 | ID 1 복습과 ID 2 예제, 둘 다 미완료 |
| 복습의 체크박스 선택 | ID 1 복습만 완료되어 체크와 취소선이 보임 |
| 예제 삭제 | ID 1 복습(완료)이 남고 ID 2만 사라짐 |
| 저장 Effect가 끝난 뒤 새로고침 | ID 1 복습(완료)이 다시 보임 |
ID는 목록의 key로 쓰여 같은 항목을 이어 줍니다. 새로고침했을 때 이전의 메모리 상태가 남는 것이 아니라, 저장된 JSON을 초기 state로 다시 읽는다는 점도 구별하세요.
직접 해 보려면 위의 App.jsx와 TodoList.jsx 코드를 각 파일에 붙여 넣고, 개발 서버가 실행 중이 아니라면 프로젝트 폴더에서 npm run dev를 실행한 뒤 터미널에 나온 Local 주소를 브라우저에서 엽니다. 새 할 일을 입력해 추가 버튼을 누르고, 체크박스로 완료 상태를 바꿔 보고, 삭제 버튼으로 할 일을 지워 보세요. 페이지를 새로고침했을 때 목록이 남는지도 확인합니다. 10장에서 다룰 React Developer Tools 확장을 설치했다면 개발자 도구의 Components 탭에서 state와 props가 바뀌는 모습도 볼 수 있습니다.
간단한 Todo 앱도 입력값, 목록, 완료 여부, 삭제 흐름을 나눠 보면 React의 상태 갱신에서 쓰는 핵심 패턴을 익히기 좋습니다.
정리
- 입력 중인 초안
newTodo와 저장할 목록todoItems를 서로 다른 state로 둡니다. - 추가는 새 항목을 붙인 배열을, 토글은
map으로 대상만 복사한 배열을, 삭제는filter로 대상을 뺀 배열을 만들어 setter에 넘깁니다. - 새 항목의 ID는 현재 목록의 가장 큰 ID에 1을 더해 만들고, 목록의
key로 씁니다. - 공백뿐인 입력은
trim()으로 걸러 내지만, 이 코드는 잘라 낸 값이 아니라 원래 입력을 저장합니다. - 저장된 목록은
useState의 초기화 함수가 읽고,[todoItems]Effect가 바뀔 때마다 로컬 스토리지에 저장합니다. - JSON을 파싱할 수 있다고 해서 Todo 배열의 형식까지 확인된 것은 아닙니다.
연습 문제
1. 지운 뒤의 새 ID
목록에 ID 4와 ID 7을 가진 두 항목이 있습니다. ID 7을 삭제한 뒤 새 항목을 추가하면 어떤 ID가 붙을까요?
해설 보기
ID 7을 지우면 현재 가장 큰 ID는 4이므로 새 ID는 5입니다. 이 규칙은 현재 목록 안에서 ID가 겹치지 않게 할 뿐, 과거에 쓴 모든 ID를 영구히 기억하는 규칙은 아닙니다.
2. 공백 세 칸
1번 다음에 입력칸에 공백 세 칸만 입력하고 추가 버튼을 누르면, 목록과 입력칸은 어떻게 될까요?
해설 보기
newTodo.trim()의 결과가 빈 문자열이므로 handleAddTodo는 일찍 반환합니다. 목록은 바뀌지 않고, 그 뒤에 있는 입력 초기화 setNewTodo('')도 실행되지 않으므로 입력칸에는 공백 세 칸이 그대로 남습니다.
3. 앞뒤에 공백이 있는 입력
앞뒤에 공백이 있는 “ 복습 ”을 입력하고 추가하면, 저장되는 text에서도 공백이 잘려 나갈까요?
해설 보기
잘려 나가지 않습니다. trim()의 결과가 비어 있지 않으므로 항목은 추가되지만, text 속성에는 원래 입력 문자열 newTodo를 저장하므로 앞뒤의 공백이 그대로 남습니다. 유효성을 판단하는 데 trim()을 쓰는 일과 잘라 낸 값을 저장하는 일은 다릅니다. 잘라 낸 값을 저장하려면 text: newTodo.trim()으로 바꿔야 합니다.
4. 저장값이 null이면
로컬 스토리지의 todoItems에 문자열 "null"이 저장되어 있을 때 이 앱을 열면 어떻게 될까요?
해설 보기
savedTodos는 빈 문자열이 아닌 "null"이므로 JSON.parse(savedTodos)가 실행되고, 그 결과인 null이 todoItems의 초기값이 됩니다. 그러면 렌더하는 동안 todoItems.length를 읽다가 TypeError가 나서 화면이 그려지지 않습니다.
null도 올바른 JSON이지만 이 예제가 요구하는 객체 배열은 아닙니다. JSON을 파싱할 수 있다는 사실만으로 형식까지 확인되지는 않으며, 손상된 데이터를 복구하거나 저장소의 권한 오류를 처리하는 일은 따로 구현해야 하는 확장 과제입니다.
이것으로 3장을 마칩니다. 3장에서는 조건에 따라 다른 화면을 보여 주는 조건부 렌더링, 배열을 목록으로 그리는 리스트와 key, state와 연결한 폼, useEffect로 다루는 컴포넌트의 생명주기를 익히고, 이를 모아 할 일 목록 앱을 만들었습니다. 다음 장의 useState 심화부터는 useState와 useEffect를 더 깊이 살펴보고, useContext와 useReducer, 커스텀 훅까지 React의 훅을 차례로 익힙니다.