본문 바로가기
Dev/React

[React] TodoList 실습으로 핵심 문법 - 데이터 전달과 CRUD

by 컴포넌트설계자 2026. 4. 8.

 

🚀 React 투두 리스트(Todo List) 구조 파헤치기: Props와 컴포넌트 계층

리액트로 프로젝트를 만들 때 가장 중요한 것은 **"데이터를 어디에 두고, 어떻게 전달할 것인가?"**이다. 오늘은 간단한 투두 리스트 구조를 통해 리액트의 핵심 동작 원리를 정리

1. 전체적인 컴포넌트 계층 구조

이미지에서 볼 수 있듯이, 서비스는 최상위 부모인 App.jsx를 중심으로 4개의 주요 컴포넌트로 나뉩니다.

  • Header: 앱의 제목이나 날짜를 표시 (정적 정보)
  • Editor: 새로운 할 일을 입력하고 등록 (onCreate)
  • List: 할 일 목록을 필터링하고 나열 (todos 데이터 사용)
  • TodoItem: 개별 할 일의 수정(onUpdate) 및 삭제(onDelete) 담당

2. 실습이 알아둬야할 개념과 문법

 

  • Component 분리: 기능별로 파일을 나누어 유지보수성을 높입니다.
  • CSS 적용: 프로젝트의 스타일을 입히는 법.
  • 이벤트 처리: 버튼 클릭(onClick), 입력 변경(onChange) 등의 핸들링.
  • Props: 부모가 자식에게 데이터를 넘겨주는 방식.
  • useState: 동적인 데이터를 관리하는 Hook.
  • useRef: DOM 요소에 직접 접근하거나 리렌더링 없이 변수를 관리할 때 사용.
  • filter & map:
    • map: todos 배열을 순회하며 여러 개의 TodoItem을 렌더링할 때 필수!
    • filter: 삭제 기능을 구현할 때 특정 아이디만 제외하는 용도로 사용.

 

1. onCreate (추가하기)

새로운 할 일을 목록에 등록하는 함수입니다.

 
  • 동작 원리: 사용자가 입력한 content를 인자로 받아 새로운 객체를 생성합니다.
     
  • 핵심 포인트: useRef를 사용하여 리렌더링과 관계없이 고유한 id 값을 유지하며 1씩 증가시킵니다.
     
  • 데이터 업데이트: 기존 todos 배열 앞에 새 항목을 추가하는 전개 연산자(...todos) 방식을 주로 사용합니다.

2. onUpdate (수정하기)

특정 할 일의 완료 여부(isDone)를 반전시키는 함수입니다.

 
  • 동작 원리: 수정 대상이 되는 targetId를 인자로 받습니다.
     
  • 핵심 포인트: 배열 메서드인 **map**을 사용하여 전체 목록을 훑으며, id가 일치하는 요소만 찾아 isDone 상태를 반전(!todo.isDone)시킵니다.
     
  • 특징: 일치하지 않는 데이터는 변형 없이 그대로 반환하여 새로운 배열을 만듭니다.

3. onDelete (삭제하기)

선택한 할 일을 목록에서 제거하는 함수입니다.

 
  • 동작 원리: 삭제할 대상의 targetId를 인자로 받습니다.
     
  • 핵심 포인트: 배열 메서드인 **filter**를 사용합니다.
     
  • 데이터 업데이트: idtargetId일치하지 않는 요소들만 모아 새로운 배열을 만듦으로써, 결과적으로 선택한 항목만 제거된 효과를 냅니다.

💡 블로그용 기술 팁: 왜 부모(App)에 만드나요?

리액트는 단방향 데이터 흐름을 따르기 때문입니다. Editor(추가)와 List(출력/수정/삭제)는 형제 관계이므로 서로 직접 데이터를 주고받을 수 없습니다. 따라서 공통 부모인 App에서 함수를 만들어 Props로 내려주는 방식을 사용합니다.

 

🚀 한 줄 요약

 
  • 추가(onCreate): 새로운 객체를 만들어 배열 맨 앞에 붙이기.
     
  • 수정(onUpdate): map으로 특정 ID만 찾아 상태 반전시키기.
     
  • 삭제(onDelete): filter로 특정 ID만 제외하고 새 배열 만들기.

Editor.JSX 

import React, { useEffect, useRef } from 'react';
import { useState } from 'react';

import './Editor.css';

function Editor({onCreate}) {//함수가 전달
     const [content, setContent] = useState("");
     const contentRef = useRef();

     //마운트 되었을 때 커서놓기
     useEffect(()=>{
         contentRef.current.focus();//커서놓기
     },[]);
   
     const onSubmit=()=>{
         if(content===""){
            contentRef.current.focus();//커서놓기
            return;
         }

         onCreate(content);//부모쪽 함수 호출!!! (입력한 내용을 전달한다.)
         setContent("");//내용비우기
         contentRef.current.focus();

     }//onSubmitEnd

     //키보드에 enter를 했을때 onSubmit을  호출해주기위한 이벤트 함수
     const onKeyDownEnter = (e)=>{
        console.log(e.keyCode);

        if(e.keyCode===13) onSubmit();
     }

    return (
        <div className="Editor">
            <input type="text" placeholder='새로운 todo' value={content} 
             onChange={(e)=>setContent(e.target.value)}  ref={contentRef}  
             onKeyDown={onKeyDownEnter}/>
            <button onClick={onSubmit}>추가</button>
        </div>
    );
}

export default Editor;

List.jsx - 검색과 리스트창

import React from 'react';
import TodoItem from './TodoItem';
import './List.css';
import { useState } from 'react';
function List({todos, onUpdate, onDelete}) {
    console.log(todos);
    const [search, setSearch] = useState("");

    //검색어에 해당하는 todo정보 또는 모든 todo를 리턴하는 함수 작성
    const getFilterData =()=>{
        if(search==="")
            return todos;
        else 
          return todos.filter((todo)=>todo.content.toLowerCase().includes(search.toLowerCase()));
    }

     const filterTodos = getFilterData();

    return (
        <div className='List'>
             <h4>Todo List 🌱</h4>
            <input placeholder='검색어를 입력해주세요.' value={search} 
            onChange={(e)=>setSearch(e.target.value)} />

            <div className='todos_wrapper'>
                 {
                    //todos.map((todo)=><TodoItem key={todo.id} {...todo} />)
                    filterTodos.map((todo)=><TodoItem key={todo.id} {...todo}  onDelete={onDelete} onUpdate={onUpdate}/>)
                 }
            </div>
        </div>
    );
}

export default List;

TodoItem.jsx

import React from 'react';
import './TodoItem.css';

function TodoItem({id, isDone, content, date, onDelete, onUpdate}) {

     const onChangeCheckbox=()=>{
       //수정하기(checkbox 상태변경)
       onUpdate(id);
    }

    return (
        <div className="TodoItem">
            <input type="checkbox" checked={isDone} onChange={onChangeCheckbox}/>
            <div className="content">{content}</div>
            <div className="date">{new Date(date).toLocaleString()}</div>
            <button onClick={()=>onDelete(id)}>삭제</button>
        </div>
    );
}

export default TodoItem;

"TodoItem.jsx는 데이터 보관소(App)로부터 받은 개별 할 일 정보를 화면에 뿌려주고, 사용자의 수정/삭제 명령을 다시 부모에게 전달하는 '현장 대리인' 역할을 합니다."

🔍 알아야 할 핵심 메커니즘: "역방향 이벤트 전달"

TodoItem은 스스로 데이터를 지울 권한이 없습니다. 데이터는 최상위 App.jsx에 있기 때문이죠. 그래서 부모로부터 받은 함수(onUpdate, onDelete)를 대신 실행함으로써 상위 컴포넌트의 상태를 변화시킵니다. 이를 전문 용어로 콜백 함수 전달이라고 합니다.

 

📑 프로젝트 요약 및 단계별 진행

Step 01. UI 및 기본 CRUD 구현: 컴포넌트(Header, Editor, List, TodoItem) 분리 및 추가, 수정, 삭제, 검색 기능 구현

Step 02. 상태 관리 업그레이드: useState의 한계를 보완하기 위해 useReducer를 도입하여 복잡한 상태 로직을 컴포넌트 외부로 분리

Step 03. 성능 최적화: useMemo, React.memo, useCallback을 사용해 불필요한 연산과 리렌더링 방지

Step 04. 전역 상태 관리: Props Drilling 문제를 해결하기 위해 useContext를 도입하고, 목적에 따라 Context를 분리하여 최적화 유지


💡 핵심 개념 TOP 5

1. useReducer: 상태 관리의 분리

  • 핵심: 컴포넌트 내부에 복잡하게 얽힌 상태 업데이트 로직을 reducer라는 별도의 함수로 빼내어 관리하는 Hook입니다.
  • 이유: App.jsx를 UI 렌더링에만 집중하게 만들고, 로직은 예측 가능한 액션(Action)을 통해 깔끔하게 처리할 수 있습니다.

2. useMemo & React.memo: 연산과 렌더링 최적화

  • useMemo: 계산 비용이 큰 함수의 결과값을 기억해 두었다가, 의존성 배열의 값이 변할 때만 다시 계산합니다.
  • React.memo: 컴포넌트 자체를 기억합니다. 부모가 리렌더링되어도 자신이 받는 props가 변하지 않았다면 리렌더링을 건너뜁니다.

3. useCallback: 함수 재생성 방지

  • 핵심: 특정 함수를 메모이제이션하여, 리렌더링 시마다 새로운 함수가 생성되는 것을 막습니다.
  • 이유: 자식 컴포넌트에게 함수를 props로 넘길 때, 함수가 매번 새로 만들어지면 React.memo가 제대로 작동하지 않기 때문에 이를 방지하기 위해 사용합니다.

4. Context API & Props Drilling

  • Props Drilling: 데이터를 사용하지 않는 중간 컴포넌트가 단순히 하위로 전달만 하는 현상으로, 구조가 깊어질수록 유지보수가 힘들어집니다.
  • Context API: 데이터를 '보관소'에 넣어두고, 필요한 컴포넌트가 어디서든 직접 꺼내 쓸 수 있게 해주는 전역 상태 관리 도구입니다.

5. Context 분리 전략 (State & Dispatch)

  • 핵심: todos(데이터)와 onCreate/onUpdate(함수) Context를 각각 분리해야 합니다.
  • 이유: 하나로 합쳐져 있으면 데이터(todos)가 변할 때마다 이를 사용하는 모든 컴포넌트(함수만 쓰는 컴포넌트 포함)가 불필요하게 리렌더링되기 때문입니다.