본문 바로가기
Frontend/React

React Todo App 만들기 (Vite + useState + 데이터 흐름 이해)

by 삐뚤비버 2025. 8. 12.

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. 삭제 버튼을 눌렀더니 전체 항목이 다 사라졌다.

원인:

  1. crypto.randomUUID 뒤에 ()가 빠져서 모든 id가 동일하게 됨
  2. setTodos 업데이트 함수에서 return이 빠져 undefined가 반환됨
  3. import { use, useState }로 use를 불필요하게 가져옴

해결:

  • `crypto.randomUUID()`로 호출
  • `setTodos`에서 `map`이나 `filter` 결과를 return
  • `useState`만 `import`

7. 상태 변화에 따른 UI 렌더링 과정

할 일 추가

  1. 입력값 변경 → `setText`로 상태 업데이트
  2. 등록 버튼 클릭 → `addTodo` 실행 → `setTodos`로 새 배열 저장
  3. 상태 변경 → `React`가 리스트 UI 재렌더링

체크박스 토글

  1. 클릭 시 해당 id를 toggle에 전달
  2. map으로 해당 항목의 done 값을 반전
  3. 상태 변경 → 해당 항목 UI만 갱신

삭제

  1. 클릭 시 해당 id를 remove에 전달
  2. filter로 해당 id 제외한 새 배열 반환
  3. 상태 변경 → 해당 항목 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

me