지난 시간에는 인라인 편집까지 마쳤습니다. 그런데 새로고침하면 데이터가 모두 사라집니다 — 모든 게 메모리에만 있고 어디에도 저장되지 않거든요. 이번 글에서는 localStorage로 영속화해서 새로고침해도 데이터가 유지되도록 만들고, 시리즈 전체를 회고하며 마무리하겠습니다.
이번 단계 목표
- 할 일 목록이 localStorage에 자동 저장됨
- 페이지 로드 시 저장된 데이터를 복원
- 필터 상태(
active,completed)도 같이 유지 - 시리즈 회고 + 다음 단계 안내
useLocalStorage 커스텀 훅
기초 강좌 #13에서 이미 만든 적이 있는 훅입니다. 그대로 가져와 씁시다.
src/hooks/useLocalStorage.js:
import { useState, useEffect } from 'react';
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// 저장 실패는 조용히 무시 (용량 초과 등)
}
}, [key, value]);
return [value, setValue];
}#13에서 만든 것에 try/catch를 추가했습니다. localStorage는 다음과 같은 실패 가능성이 있어요.
- JSON.parse 실패 — 다른 코드가 저장한 잘못된 데이터가 들어 있을 때
- localStorage.setItem 실패 — 저장 용량 초과(보통 5MB), 시크릿 모드 일부 환경 등
이런 경우에 앱 전체가 깨지는 것보다 조용히 fallback 하는 편이 사용자 경험이 좋습니다.
초기값 인자에 함수를 넘긴 점(useState(() => { ... }))도 다시 짚어두면, 이건 #5에서 살짝 언급했던 "지연 초기화(lazy initializer)" 패턴입니다. useState(localStorage.getItem(...))처럼 직접 호출하면 매 렌더링마다 localStorage를 읽지만, 함수로 감싸면 첫 렌더링 시 한 번만 실행됩니다. localStorage 같이 비용이 있는 작업의 초기화에 적합합니다.
TodoApp에 적용
useState를 useLocalStorage로 바꾸기만 하면 됩니다. 인터페이스가 똑같으니 다른 코드는 손대지 않아도 돼요.
src/TodoApp.jsx 변경 부분:
import { useState } from 'react';
import { useLocalStorage } from './hooks/useLocalStorage';
// ... 다른 import들 ...
function TodoApp() {
const [todos, setTodos] = useLocalStorage('todos', []);
const [filter, setFilter] = useLocalStorage('todoFilter', 'all');
const [editingId, setEditingId] = useState(null);
// ... 나머지 그대로 ...
}todos와 filter는 영속화 대상이고, editingId는 일시적인 UI 상태이니 그대로 useState로 둡니다 — 페이지를 새로고침했는데 편집 모드가 그대로 떠 있으면 오히려 어색하니까요.
키 이름('todos', 'todoFilter')은 localStorage에 저장되는 식별자입니다. 다른 앱과 충돌하지 않도록 충분히 명확한 이름으로 두는 게 좋아요. 큰 앱에서는 'myapp:todos'처럼 prefix를 붙이는 관례도 있습니다.
동작 확인
저장하고 다음을 시도해보세요.
- 할 일 몇 개 추가하고 일부를 완료 표시
- 필터를 "미완료"로 변경
- 페이지 새로고침 (Cmd+R 또는 F5)
- 모든 할 일과 필터 상태가 그대로 복원됨
- 브라우저 개발자 도구 → Application 탭 → Local Storage → 도메인 →
'todos','todoFilter'키가 있는지 확인
새 탭에서 같은 페이지를 열어도 (같은 도메인이면) localStorage가 공유되니 데이터가 그대로 보입니다.
비어 있는 상태로 시작 vs 데모 데이터
처음 방문한 사용자에게 빈 화면을 보여주면 "어떻게 쓰는 건지" 막막할 수 있습니다. 시드(seed) 데이터를 넣을지 결정해야 할 때가 있죠. 옵션은:
A. 빈 상태로 시작 + 안내 문구 (현재)
- 단순하고 명확
- "할 일이 없습니다. 새로 추가해보세요." 안내가 빈 상태 처리 역할
B. 데모 데이터로 시작
const [todos, setTodos] = useLocalStorage('todos', [
{ id: 'demo-1', text: '리액트 공부하기', completed: true },
{ id: 'demo-2', text: '운동 30분', completed: false },
]);- 사용자가 바로 기능을 둘러볼 수 있음
- 단점: 데모 데이터를 일일이 지워야 함
이번 시리즈는 A로 둡니다. 둘 다 합리적이니 본인 취향에 맞게 선택하세요.
다른 탭과의 동기화 (선택)
같은 도메인의 두 탭을 열어두고 한쪽에서 할 일을 추가하면, 다른 탭은 새로고침할 때까지 변화를 모릅니다. localStorage 자체는 변경됐지만 리액트는 그걸 모르거든요.
브라우저는 localStorage가 다른 탭에서 변경되면 storage 이벤트를 발생시킵니다. 이걸 구독하면 자동 동기화가 가능합니다.
useEffect(() => {
function handleStorage(e) {
if (e.key === key && e.newValue !== null) {
try {
setValue(JSON.parse(e.newValue));
} catch {
// ignore
}
}
}
window.addEventListener('storage', handleStorage);
return () => window.removeEventListener('storage', handleStorage);
}, [key]);기능이 필요한 시점에 추가해도 충분합니다. 이번 시리즈에선 단순화를 위해 생략했어요.
그 외 개선 아이디어
여기까지 만들고 나면 본인이 직접 더 발전시킬 거리가 많이 보일 것입니다.
- 시각적 다듬기: 인라인 스타일을 CSS Modules / Tailwind / styled-components로 분리. 모바일 반응형
- 드래그로 순서 변경:
react-dnd나@dnd-kit/core사용 - 카테고리/태그: 할 일에 태그를 달고 필터에 추가
- 마감일: 날짜 필드 추가, 지난 항목 강조
- 검색: 입력 텍스트로 즉시 필터링 (#13의
useDebounce활용 좋은 기회) - 다크 모드: #12에서 만든 ThemeContext 적용
- TypeScript 마이그레이션: 항목 객체 타입을 명시해 안전성 ↑
- 테스트: Vitest + React Testing Library로 핵심 동작 단위 테스트
- 백엔드 연동: 정말로 동기화하려면 서버가 필요. JSON Server나 Supabase 같은 BaaS로 실험 가능
각각이 좋은 학습 거리이고, 작은 앱일수록 실험하기 부담이 없습니다. 만들고 싶은 게 있으면 시도해보세요.
시리즈 회고
이 시리즈에서 우리는 다음을 만들었습니다.
| # | 추가된 기능 | 등장한 핵심 패턴/도구 |
|---|---|---|
| 1 | 추가/삭제, 컴포넌트 분해 | 단방향 데이터 흐름, 상태 끌어올리기, UUID, controlled form |
| 2 | 완료 토글, 통계 | 불변 업데이트(map 패턴), 파생 값 |
| 3 | 필터링, 일괄 삭제 | 데이터 배열로 옵션 렌더, 빈 상태 분기 |
| 4 | 인라인 편집 | useRef, draft state, 키보드 처리, onBlur |
| 5 | localStorage 영속화 | 커스텀 훅 재사용, lazy initializer |
순서대로 따라 오셨다면 단순한 컴포넌트 하나에서 시작해 작은 실전 앱이 만들어지는 과정을 직접 체험하셨을 것입니다.
기초 강좌에서 배운 것들이 어떻게 합쳐졌나
이번 시리즈에서 거의 모든 기초 개념이 자연스럽게 등장했습니다.
- 컴포넌트와 props (#4) — TodoForm, TodoItem, TodoList, TodoStats, TodoFilter로 화면을 책임 단위로 분해
- useState (#5) — todos, filter, editingId, draft 등 모든 변하는 데이터
- 이벤트 핸들링 (#6) — onClick, onChange, onSubmit, onKeyDown, onBlur
- 조건부 렌더링 (#7) — 빈 상태, 편집 모드 분기, 일괄 삭제 버튼
- 리스트와 key (#8) — todos를 map으로 그리고 UUID를 key로
- 폼 다루기 (#9) — controlled component 패턴, 체크박스
- useEffect (#10) — localStorage 동기화, 편집 모드 진입 시 포커스
- 상태 끌어올리기 (#11) — todos는 TodoApp에, 자식들은 콜백으로 알림
- useContext (#12) — 등장하지 않았음 (이 규모에선 prop 전달이 더 명확)
- 커스텀 훅 (#13) — useLocalStorage
- 성능 최적화 (#14) — 의도적으로 최적화 안 함 (현재 규모엔 불필요)
- 라우팅 (#15) — 단일 화면이라 미사용 (멀티 페이지 앱이라면 등장)
모든 기초 개념이 매번 등장하지는 않습니다. 필요한 도구를 그 상황에 맞게 골라 쓰는 감각이 점차 생기는 게 더 중요해요. 한 가지 도구로 모든 걸 해결하려 하기보다, "이 상황엔 이게 더 맞다"고 판단할 수 있게 되는 게 진짜 실력입니다.
다음 단계 추천
이 시리즈를 마치셨다면 다음 중 하나로 넘어가시면 좋습니다.
A. 또 다른 작은 프로젝트 만들기 (가장 추천)
직접 작은 앱을 하나 더 만들어보세요. 본인의 일상에 도움 되는 작은 도구가 가장 좋습니다.
- 운동 기록기 (어제 한 횟수와 비교)
- 습관 추적기 (체크 박스 달력)
- 메모장 (markdown 지원, 검색)
- 가계부 (월별 합계)
- 독서 기록 (책 + 메모)
기능을 욕심내기보다 첫 버전을 빠르게 완성 → 사용해보면서 개선하는 사이클이 학습 속도가 빠릅니다.
B. 모던 리액트 19 + Next.js 시리즈 (예정)
이 블로그의 다음 시리즈로, Server Components / use() / Actions / Suspense 같은 최신 패턴을 다룰 예정입니다. 클라이언트 사이드만으로는 부족한 영역들을 어떻게 풀어내는지 보게 됩니다.
C. 좀 더 큰 실전 빌드 (예정)
블로그, 쇼핑몰처럼 라우팅 + 상태 관리 + 데이터 페칭이 모두 들어가는 더 큰 프로젝트.
마무리
여기까지 따라와주셔서 감사합니다. 단순한 입력창 하나에서 시작해, 컴포넌트 분리 → 토글 → 필터링 → 편집 → 영속화까지 작은 실전 앱이 완성됐습니다. 처음에는 막막해 보이던 것들이 차례차례 더해지면서 손에 익었을 거예요.
리액트는 도구일 뿐입니다. 우리가 만든 게 화려하진 않아도 머릿속의 아이디어를 손으로 만들어내는 경험을 했다는 게 가장 큰 수확입니다. 그 감각이 쌓이면 어떤 라이브러리, 어떤 프레임워크라도 빠르게 익힐 수 있게 됩니다.
다음 시리즈에서 또 만나요!