React Todo App 만들기 (Vite + useState + 데이터 흐름 이해)
1. 프로젝트 목표
- 할 일 등록 / 완료 체크 / 삭제 기능이 있는 간단한 Todo 앱 구현
- React의 상태 관리(State)와 데이터 흐름 원리 이해
- 차후 클린코드 리팩토링 준비
2. 개발 환경 세팅
Vite로 프로젝트 생성
cd "D:\react-toy\todo"
npm create vite@latest . -- --template react
npm install
npm run dev
- npm run dev 실행 후 표시된 로컬 주소로 접속하면 React 기본 페이지 확인 가능
- VS Code로 열어서 개발 진행
3. React 기본 구조 이해
src/
├─ main.jsx // 앱 진입점, ReactDOM이 App 컴포넌트를 HTML에 연결
├─ App.jsx // 메인 컴포넌트(UI와 기능 중심)
├─ index.css // 전체 스타일
- index.html의 <div id="root"></div>가 React 앱이 그려질 자리
- main.jsx에서 ReactDOM.createRoot로 App 컴포넌트를 root에 렌더링
- App.jsx는 실제 UI와 로직을 담는 첫 번째 컴포넌트
4. 단방향 데이터 흐름
React는 부모에서 자식으로만 데이터가 내려가고, 자식에서 부모로는 이벤트를 통해 전달한다.
상태(State)가 변경되면 React가 해당 UI만 다시 그린다.
예시 흐름:
[사용자 입력]
↓ onChange
[text 상태 업데이트]
↓
[폼 제출]
↓ onSubmit
[todos 상태 업데이트]
↓
[UI 재렌더링]
5. Todo App 코드 (첫 번째 버전)
// useState 훅을 React에서 가져오기
import { useState } from "react";
// App 컴포넌트 - Todo 앱의 메인 컴포넌트
export default function App() {
// text: 현재 입력창 값, setText: text 상태 변경 함수
const [text, setText] = useState(""); // 초기값은 빈 문자열
// todos: 할 일 목록 배열, setTodos: todos 상태 변경 함수
const [todos, setTodos] = useState([]); // 초기값은 빈 배열
// 새로운 할 일 추가 함수
const addTodo = (e) => {
e.preventDefault(); // form 기본 동작(새로고침) 방지
const value = text.trim(); // 입력값 공백 제거
if (!value) return; // 빈 값이면 추가하지 않음
// 기존 todos 배열에 새 객체(id, text, done 상태) 추가
setTodos((prev) => [
...prev,
{ id: crypto.randomUUID(), text: value, done: false },
]);
setText(""); // 입력창 비우기
};
// 완료 상태 토글 함수
const toggle = (id) => {
// todos 배열에서 해당 id의 done 값을 반전
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
};
// 할 일 삭제 함수
const remove = (id) => {
// todos 배열에서 해당 id와 다른 항목만 남김
setTodos((prev) => prev.filter((todo) => todo.id !== id));
};
// 컴포넌트 UI 렌더링
return (
<div style={{ maxWidth: 500, margin: "4em auto" }}>
<h1>TODO APP</h1>
{/* 할 일 입력 폼 */}
<form onSubmit={addTodo}>
{/* 입력값이 변경될 때마다 text 상태 업데이트 */}
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">추가</button>
</form>
{/* 할 일 목록 */}
<ul>
{todos.map(({ id, text, done }) => (
<li key={id}>
{/* 완료 체크박스 - 클릭 시 해당 id의 done 상태 토글 */}
<input
type="checkbox"
checked={done}
onChange={() => toggle(id)}
/>
{/* 완료된 항목은 취소선 표시 */}
<span style={{ textDecoration: done ? "line-through" : "none" }}>
{text}
</span>
{/* 삭제 버튼 - 클릭 시 해당 id 항목 삭제 */}
<button onClick={() => remove(id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
6. 필수 개념 정리
1. 컴포넌트(Component)
- React 앱을 구성하는 독립적인 UI 조각
- 함수형 컴포넌트 형태로 작성:
export default function App() { ... }
- 각 컴포넌트는 **자신만의 상태(State)**와 이벤트 처리 로직을 가질 수 있음
2. 상태 관리(useState)
- useState는 컴포넌트 내부에서 데이터를 기억하고 관리하는 훅(Hook)
const [value, setValue] = useState(초기값);
- 구조 분해 할당:
- value: 현재 상태 값
- setValue: 상태를 변경하는 함수
- 상태가 변경되면 React는 해당 컴포넌트를 다시 렌더링함
3. 이벤트 핸들러
- JSX에서 이벤트는 카멜 케이스로 작성 (onClick, onChange, onSubmit)
- 함수 전달 방식에 따라 사용법이 달라짐:
- 함수명만 전달 → 이벤트 객체를 그대로 받음 (onSubmit={addTodo})
- (e) => ... → 이벤트 객체에서 값 꺼내서 전달 (onChange={(e) => setText(e.target.value)})
- () => ... → 다른 데이터 전달 (onClick={() => remove(id)})
4. 불변성(Immutable) 유지
- React 상태는 직접 수정하지 않고, 새로운 배열/객체를 만들어서 변경해야 함
- 예:
// 잘못된 예
todos.push(newTodo); // 원본 배열 변경
setTodos(todos);
// 올바른 예
setTodos([...todos, newTodo]); // 새로운 배열 생성
5. 리스트 렌더링과 key 속성
- map을 이용해 배열 데이터를 JSX로 변환
- 각 항목에는 key 속성이 필요 → React가 항목을 안정적으로 식별
- 이 코드에서는 crypto.randomUUID()로 고유 ID 생성
6. 조건부 스타일링
- JSX에서 스타일을 동적으로 바꾸려면 객체 형태로 작성
- 예:
<span style={{ textDecoration: done ? "line-through" : "none" }}>
7. form과 e.preventDefault()
- HTML `<form>`은 기본적으로 submit 시 페이지를 새로고침
- React에서는 `e.preventDefault()`로 이 동작을 막고, JavaScript로만 처리
7. 토이 프로젝트 구현시 궁금증
Q. `crypto.randomUUID()`는 무엇인가?
브라우저 내장 API로, 전 세계적으로 겹치지 않는 고유 식별자(UUID)를 생성한다.
React에서 리스트 렌더링 시 key 값으로 사용하면 각 항목을 안정적으로 식별할 수 있다.
Q.
왜`<form onSubmit={addTodo}>` 는 함수명만 그대로 쓰는데,
`<input onChange={(e) => setText(e.target.value)}>`나
`<input onChange={() => toggle(id)}>`는 화살표 함수를 쓰나요?
- 함수명만 전달: 이벤트 객체를 그대로 받는 경우 (예: onSubmit)
- (e) => ...: 이벤트 객체에서 필요한 값을 꺼내서 함수에 전달해야 하는 경우
- () => ...: 이벤트 객체 대신 다른 값을 전달해야 하는 경우
| 작성 방식 | 설명 | 예시 |
| onEvent={함수명} | 이벤트 객체를 그대로 함수로 전달할 때 사용 | <form onSubmit={addTodo}> |
| onEvent={(e) => 함수(e.target.value)} | 이벤트 객체에서 특정 값을 꺼내서 전달할 때 사용 | <input onChange={(e) => setText(e.target.value)}> |
| onEvent={() => 함수(다른값)} | 이벤트 객체 대신 다른 데이터(예: id)를 전달할 때 사용 | <input type="checkbox" onChange={() => toggle(id)}> |
Q. 삭제 버튼을 눌렀더니 전체 항목이 다 사라졌다.
원인:
- crypto.randomUUID 뒤에 ()가 빠져서 모든 id가 동일하게 됨
- setTodos 업데이트 함수에서 return이 빠져 undefined가 반환됨
- import { use, useState }로 use를 불필요하게 가져옴
해결:
- `crypto.randomUUID()`로 호출
- `setTodos`에서 `map`이나 `filter` 결과를 return
- `useState`만 `import`
7. 상태 변화에 따른 UI 렌더링 과정
할 일 추가
- 입력값 변경 → `setText`로 상태 업데이트
- 등록 버튼 클릭 → `addTodo` 실행 → `setTodos`로 새 배열 저장
- 상태 변경 → `React`가 리스트 UI 재렌더링
체크박스 토글
- 클릭 시 해당 id를 toggle에 전달
- map으로 해당 항목의 done 값을 반전
- 상태 변경 → 해당 항목 UI만 갱신
삭제
- 클릭 시 해당 id를 remove에 전달
- filter로 해당 id 제외한 새 배열 반환
- 상태 변경 → 해당 항목 UI에서 제거
8. 핵심 정리
- React 상태는 불변성을 지켜야 하며, 기존 배열·객체를 직접 수정하지 않고 새로 만들어 반환한다.
- 이벤트 핸들러 작성 방식은 전달할 데이터의 형태에 따라 달라진다.
- `crypto.randomUUID()`는 간단하게 고유 id를 생성할 수 있는 방법이다.
- 상태가 바뀌면 React는 변경된 부분만 UI를 다시 그린다.
'Frontend > React' 카테고리의 다른 글
| Vite + React + Tailwind 설치 가이드 (0) | 2025.08.22 |
|---|---|
| Vite와 Create React App(CRA) 비교 (1) | 2025.08.11 |
| 생활코딩 React useReducer 실습 정리 (1) | 2025.07.27 |
| React useReducer (2) | 2025.07.27 |
| React State (0) | 2025.07.19 |