Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_30일차_"리액트 Todo"

LEFT 2025. 1. 14. 18:44

🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [30]일차

🚀30일차에는 리액트로 Todo리스트를 구현하면서 추가, 삭제, 수정 기능을 어떻게 구현하면 좋을지 배울 수 있었다.

useState가 중요하게 자주 쓰였고 props로 함수객체와 데이터객체를 전달하여

다른 컴포넌트에서 수행한 값을 전달할 수 있었다. 회고를 통해 코드를 다시 해석해봐야겠다고 생각했다.


 

Todo 리스트 만들기

  • 전체 컴포넌트를 감싸는 컴포넌트 = TodoBox 컴포넌트
  • 그 안에서 Input을 받는 Todo = TodoInput 컴포넌트
  • Todo들을 보여주는 컴포넌트 = TodoList 컴포넌트

추가 기능

  • 할 일 입력 후 엔터 입력 시 “할일이 등록되면서” 리스트에 정보 출력 (input태그에 이벤트onKeyDown 활용)
  • 리스트의 항목들을 배열로 관리
  • 리스트가 "상태관리"되어야함
const [toDoList, setTodoList] = useState([
    { id: 1, title: "리액트 공부하기" },
    { id: 2, title: "스프링 공부하기" },
    { id: 3, title: "블로그 회고작성" },
  ]);
  • 데이터를 가진 배열은 TodoList와 TodoInput컴포넌트 둘다 접근이 가능한 TodoBox 컴포넌트가 관리해야함
  • 이 데이터 배열을 props를 통해 객체로써 각각의 컴포넌트에 보내줄 수 있을 것이다.

 

const TodoList = ({ todoList }) => {
  return (
    <ul>
      {todoList.map((toDo) => (
        <li key={toDo.id}>{toDo.title}</li>
      ))}
    </ul>
  );
};

export default TodoList;
  • const TodoList = ( { todoList })
    ➡️ TodoBox 컴포넌트로부터 "데이터 배열"을 전달받아서 사용

  • return <ul> ... </ul>
    ➡️<ul>태그 밑에 { } 자바스크립트 코드로 감싼 map()함수를 사용하여 <li>태그를 만든다.

  • <li key={todDo.id}>
    ➡️map 사용 시 "기본키 값이 필요"하므로 id값을 키 값으로 설정한다.

 

// TodoBox컴포넌트
const addTodoList = (title) => {
    console.log(title);
  };

  return (
    <div>
      <TodoInput addTodo={addTodoList} />
      <TodoList todoList={todoList} />
    </div>
  );
  • TodoInput 컴포넌트에서 사용자에게 값을 입력받은 후 수정하는 로직은
    TodoBox컴포넌트에서 관리해야할 것이 맞을 것이다.
  • TodoBox컴포넌트에서 TodoInput컴포넌트로 props를 넘길때
    "addTodo" 라는 “키”를 넘겨야한다. ⚠️"addTodoList"라는 “value”를 넘기면 안된다.
// TodoInput 컴포넌트
const TodoInput = ({ addTodo }) => {
  const [toDo, setToDo] = useState("");

  const keyDownHandler = (e) => {
    if (e.key === "Enter") {
      // Enter가 입력되면 리스트에 값을 저장
      if (e.target.value === "") return;
      addTodo(e.target.value); // 실제 리스트에 저장하는 부분은 TodoBox컴포넌트가 관리
      setToDo("");
    }
  };
  //...
  • TodoBox컴포넌트로부터 addTodo “키”를 props로 전달받아서 keyDownHandler() 에서 사용

  • addTodo(e.target.value)
    ➡️TodoBox컴포넌트에 e.target.value 를 "인자"넘기게된다.
    ➡️즉 TodoInput 컴포넌트에서 keyDownHandler 이벤트 수행을 통해
    TodoBox컴포넌트의 함수인 addTodo()에게 (e.target.value)인자를 전달하게 되는 것이다.

  • setToDo("")
    ➡️값이 전달되면 “” 로 초기화시킨다. (useState의 set함수)

  • TodoInput 컴포넌트에서 사용자에게 값을 입력받을때 title값만 입력을 받지만
    TodoBox컴포넌트에서 "데이터 배열"을 관리하기 위해서는 배열에 있는 속성인 id값도 필요할 것이다.

  • 그러기 위해서는 id값을 1씩 증가하면서 값을 넣기위해 useRef를 사용할 수 있다.
    (내부적으로 값이 계속 변할 수 있는 기능제공 ↔ ⚠️let 사용 X (리렌더링 시 값이 초기화됨)
    ➡️useRef로 관리하는 변수는 "값이 바뀌어도 컴포넌트가 리렌더링되지 않는 특징"을 활용

 


Todo리스트 - 할 일 "추가" 구현

 

▶️실습 - TodoBox 컴포넌트 : useRef()를 활용한 id값 추가

const id = useRef(Math.max(...todoList.map((todo) => todo.id)) + 1);

const addTodoList = (title) => {
  console.log("새로 추가한 값 : id[" + id + "], title[" + title + "]");
  const newTodo = {
    id: id.current,
    title: title,
  };
  setTodoList([...todoList, newTodo]);
  id.current += 1;
};
  • const newTodo = { id: id.current, title:title };
    ➡️새로운 값을 넣기위한 배열을 만든다.
    ➡️id(key), 값(value)의 구조로된 배열로 기존 배열과 동일한 구조를 가진다.

  • id.current 의미
    ➡️useRef의 현재 값을 가져온다. (현재 id값)
    ➡️useRef의 구조는 { current:0 } 처럼 "객체"로 만들어져있다.
    id.current대신 id를 참조하면 { current : 0 } 처럼 객체 자체가 리턴되므로
    id.current를 참조해주어야 id 객체의 current를 직접 참조하여 그 값을 가져올 수 있게되는 것

    ⚠️useRef사용시에는 current를 사용해야 useRef의 값을 사용 가능

  • setTodoList([…todoList, newTodo]);
    ➡️useState의 set함수(setTodoList)를 사용하여 (기존 배열의 값들 + 새로운 배열)로 TodoList 배열을 업데이트
    ➡️(… 스프레드 연산자)를 사용하여 기존배열(todoList)의 값 요소만 가져와야한다.

    👀 위 코드 대신 사용할 수 있는 코드로
    setTodoList(toDoList.concat(newTodo));

    ➡️concat()를 활용해 기존 배열 toDoList에 concat을 통해서 newTodo라는 새로운 배열을 이어 붙여줄 수 있다.

Math.max(...todoList.map((todo) => todo.id))
  • "데이터 배열"의 id값이 반드시 (1, 2, 3, 4) 이지 않을 수 있고, 특정 id값(ex.3번)이 삭제된 상태일 수도 있다.
  • id는 primary key이므로 중간 id값이 비어있어도 오류로 발생되진 않아 문제를 찾기 힘들 수 있다.

  •  
  • map()을 활용하여 현재 "데이터 배열"의 최대 id값을 구해야한다.

  • Math.max()
    ➡️만약 todoList.map()을 사용하게되면 배열 자체를 가져오게되므로
    max()함수가 객체에 대한 최대값을 반환하지 못한다.

    ➡️해결방법 : …배열(스프레드 문법)을 활용한다 (ex. 1, 2, 3, 4 중 최대값을 추출해낼 수 있다. ( "4" ))

 

▶️실습 - TodoInput 컴포넌트 : keyDownHandler() 수정

// TodoInput 컴포넌트 - keyDownHandler()에 추가
e.target.value("");
e.target.focus();
  • e.target.value(""); ➡️할 일을 입력(Enter) 한 후 input 박스에 입력했던 값이 비워지도록 구현
  • e.target.focus(); ➡️비워진 후에는 focus()를 통해 비워진 input박스에 커서가 위치할 수 있도록 구현

Todo리스트 - 할 일 "수정/삭제" 구현

  • 수정, 삭제는 “데이터 배열"(=리스트)가 수정, 삭제 되어야하므로
    "데이터 배열"(=리스트)가 담긴 TodoBox컴포넌트에서 수행

▶️실습 - TodoBox컴포넌트 : deleteTodoList() = 삭제 기능

// TodoBox 컴포넌트 

// 2. 할 일 삭제 (추가)
const deleteTodoList = (id) => {
	setTodoList(todoList.filter((todo) => todo.id !== id));
}
  • const deleteTodoList = (id) => { }
    ➡️id값을 가져와서 그 id에 해당하는 할 일을 삭제할 수 있도록 구현

  • todoList.filter()
    ⭐filter()함수를 활용
    ➡️조건을 만족하는 것만 뽑아내어 새로운 배열을 리턴해줌 (map()과의 차이점은 조건을 검사하는지 안하는지)

  • todo.id !== id
    ➡️filter()함수의 조건식을 의미
    현재 TodoBox컴포넌트에서 useRef로 관리되고 있는 todo.iddeleteTodoList 함수에서 전달받은 id를 비교
    ➡️filter()를 통해 id값이 일치하지 않는 것만 새로운 배열로 만들어냄
    (즉 삭제할 할 일의 id값을 제외하고 새로운 배열로 만들어냄)

    ➡️삭제할 할 일을 제외하고 새 배열로 만들어지므로 “삭제하는 듯한 기능이 구현됨”

▶️실습 - TodoBox컴포넌트 : updateTodoList() = 수정 (리렌더링 X)

// TodoBox 컴포넌트

// 3. 할 일 수정 (리렌더링 X)
const updateTodoList = (todo) => {
	todoList.map((item) => {
    if (item.id === todo.id) {
      item.title = todo.title;
    }
  });
}
  • 할 일 수정 후 리렌더링이 필요하지 않을때의 코드

  • "수정"의 기능을 하는 컴포넌트로부터 todo라는 객체를 받아와서 id와 title을 모두 관리할 수 있도록 함
    💡todo 객체 { id: undefined, title : undefined}

  • map( (item) => { ... })
    ➡️TodoList에서 요소들을 모두 꺼내서 진행해야하므로 map() 사용

  • item ➡️임의로 이름을 설정한 todo 객체

  • item.id === todo.id
    ➡️item.id은 현재 TodoBox컴포넌트에서 관리되고 있는 리스트의 id를 가리킨다.
    ➡️todo.id는 updateTodoList 함수에서 전달받은 todo객체의 id를 가리킨다.

    따라서 이 둘이 같으면 현재 TodoBox컴포넌트의 리스트의 title을
    새롭게 받아온 todo객체의 title로 바꿔주는 코드를 의미

▶️실습 - TodoBox컴포넌트 : updateTodoList() = 수정 (리렌더링 O)

// TodoBox 컴포넌트

// 3. 할 일 수정 (리렌더링 O)
const updateTodoList = (todo) => {
	const updating = todoList.map((item) => {
		item.id === todo.id ? {...item, title:todo.title} : item;
	});
	setTodoList(updating);
}
  • 할 일 수정 후 리렌더링이 필요할때의 코드

  • item.id === todo.id ? {...item, title:todo.title} : item;
    ➡️삼항연산자를 활용해 item.id === todo.id 가 일치한다면 {...item, title:todo.title} 수행

  • {…item, title:todo.title}
    ➡️조건식 (item.id === todo.id)가 true일때 실행하는 문장이다.
    ...item는 기존 배열의 요소들을 updating에 저장하고
    title:todo.title에서 title은 todo.title로 갱신하는 기능을 한다.

  • item
    ➡️조건식 (item.id === todo.id)가 false일때 실행하는 문장이다.
    수정할 id가 기존 배열에 존재하지 않을때 기존 배열만 그대로 updating에 저장

  • setTodoList(updating);
    ➡️useState의 set함수를 이용해 위의 조건으로부터 수정되었던 배열(updating)을 업데이트 시킨다.

▶️실습 - 삭제 기능 적용

 

TodoBox컴포넌트

// TodoBox컴포넌트
return (
  <div>
    <TodoInput addTodo={addTodoList} />
    <TodoList
      todoList={todoList}
      deleteHandler={deleteTodoList}
      updateHandler={updateTodoList}
    />
  </div>
);
  • TodoBox 컴포넌트의 <TodoList>컴포넌트가 담긴 부분에서 deleteHandler와 updateHandler에 각각의 함수를 전달
    ➡️TodoList 컴포넌트에서 deleteHandler와 updateHandler를 props로 전달받아 사용 가능

 

TodoList컴포넌트

// TodoList컴포넌트
const TodoList = ({ todoList, deleteHandler, updateHandler}) => {
  return (
    <ul>
      {todoList.map((toDo) => (
        <li key={toDo.id}>{toDo.title}</li>
      ))}
    </ul>
  );
};

export default TodoList;

  • const TodoList = ( { todoList, deleteHandler, updateHandler } ) => { ... }
    ➡️props로 "데이터 배열"과 deleteHandler, updateHandler를 전달받음
    하지만 이렇게되면 TodoList 컴포넌트에서 해야하는 기능이 많아질 수 있으므로 “컴포넌트 분리”를 고려

Todo 컴포넌트로 기능 분리

const Todo = () => {
    return(
        
    );
}

export default Todo;
  • return은 <div>처럼 묶여있어야 사용할 수 있는데
    이 경우 Todo컴포넌트를 다른 컴포넌트에서 사용하게될때 <div>가 묶인 상태로 전달받게되므로
    <div>로 한번 더 감싸져 원치않는 결과가 나올 수 있다.

  • ➡️해결방법 : Fragment (<> </>) 사용
    return문에서 묶는 기능을 하면서도 태그가 전달되진 않아 원하는 결과를 얻을 수 있다.

▶️실습 - Todo 컴포넌트 : 삭제 버튼 만들기

const Todo = ({todo, deleteHandler}) => {
    return(
        <>
            {todo.title}
            <button onClick={() => deleteHandler(todo.id)}>삭제</button>
        </>
    );
}

export default Todo;
  • {todo.title}
    ➡️TodoList컴포넌트로부터 받아온 todo객체으로부터 title을 만들어줌

  • onClick={ () => deleteHandler(todo.id) }
    ➡️props로 받아온 deleteHandler함수 안에 todo.id를 넘겨준다. (TodoBox컴포넌트까지 전달됨)

 

❓onClick = {deleteHandler(todo.id)}
onClick={ () ⇒ deleteHandler(todo.id)} 의 차이점

➡️실행 시점의 차이이다.
- deleteHandler(todo.id)
함수 호출 결과를 `onClick`에 전달하는 방식으로 "컴포넌트가 렌더링되는 시점에 함수가 즉시 실행"된다.
따라서 deleteHandler(todo.id)의 반환값이 onClick에 할당되므로 이벤트와는 상관없이 "렌더링 중에 함수가 실행"

- onClick={() => deleteHandler(todo.id)}
람다 함수를 사용해 deleteHandler(todo.id) 함수를 호출할 수 있다.
onClick이벤트가 발생했을때 "() 람다함수"가 실행되어 deleteHandler(todo.id)를 호출하는 것이기때문에
컴포넌트가 렌더링되는 시점에는 실행되지 않고, onClick 이벤트가 발생할때 deleteHandler()함수가 실행된다.

람다 함수 ( () => )를 사용해야 이벤트가 발생할 때 원하는 함수가 실행된다.


▶️실습 - TodoList 컴포넌트 : 삭제 기능을 Todo컴포넌트에 적용

import Todo from "./Todo";

const TodoList = ({ todoList, deleteHandler, updateHandler}) => {
  return (
    <ul>
      {todoList.map((toDo) => (
        <li key={toDo.id}><Todo todo={toDo} deleteHandler={deleteHandler}/></li>
      ))}
    </ul>
  );
};

export default TodoList;

  • TodoList 컴포넌트에서는 {toDo.title}부분을 <Todo/>컴포넌트로 대체가능
    ➡️(Todo컴포넌트에서 <> 버튼 </>을 return하게 되므로)

  • 속성으로는 map()을 통해 관리한 값인 toDo가 담겨있는 todo 속성을 전달

  • deleteHandler={deleteHandler};
    ➡️ToboBox컴포넌트로부터 받아온 deleteHandler를 다시 Todo컴포넌트로 넘겨주는 역할을 한다.

  • 삭제 버튼을 클릭하면 deleteHandler가 발생하여
    TodoBox컴포넌트의 리스트가 수정되는 것까지 구현

▶️실습 - Todo 컴포넌트 : 수정 버튼 만들기

  • 요청이 2개일 것이다. “수정할 폼(틀)” 요청 “수정한 값으로 전달”하는 요청을 수행해야한다.
    ex. 마치 로그인에서 “로그인 폼(틀)” 요청과 “로그인 요청” 두개 인 것과 유사하다.

  • 위의 그림에서 위에 있는 수정버튼은 “수정해달라”라는 요청이고
    밑에 있는 수정버튼은 “수정 폼”을 요청하는 요청이다.

▶️실습 - TodoList 컴포넌트 : 수정 기능을 Todo컴포넌트에 적용

import { useState } from "react";

const Todo = ({ todo, deleteHandler }) => {

  const [updateMode, setUpdateMode] = useState(false); // 기본값은 false

  return (
    <>
      {todo.title}
      <button onClick={() => deleteHandler(todo.id)}>삭제</button>
      <button>수정</button>
    </>
  );
};

export default Todo;

  • 수정폼 요청 부분과과 수정 요청 부분을 추가
  • const [updateMode, setUpdateMode] = useState(false);
    ➡️updateMode를 useState로 상태를 관리하여 updateMode가 true바뀌면 수정폼이 보여지게끔 구현

  • 수정버튼이 클릭되면 수정 폼이 요청되어 수정폼을 보여줄 수 있도록 하는 기능을 추가 구현해야한다.
    ➡️updateMode를 true로 바꿔주어 수정 폼이 보여지는 기능 필요

<button onClick={updateModeHanlder}>수정</button> 호출 방식에 대하여

삭제 기능을 구현할때 { () => deleteHandler(todo.id) } 처럼 람다를 활용한 방식과 호출방식이 똑같다. 

그 이유는 updateModeHandler에 따로 매개변수를 전달하지 않기 때문에, 별도의 람다함수(() =>)가 필요없는 것이다.

따라서 <button onClick={updateModeHandler}>는 이벤트가 발생할때만 이벤트핸들러가 실행되므로
람다 함수 없이도 적절하게 동작한다.


onChange 속성

  • 리액트에서 <input> 에 value속성을 설정했을때,
    onChange 속성을 제공하지 않으면 "읽기 전용"으로 판단되기때문에 onChange속성을 적용해주어야한다.
  • 리액트에서 value 속성이 설정되면 "제어 컴포넌트"로 판단되어 입력 값이 부모 컴포넌트의 상태에 의해 제어된다.
  • 💡따라서 onChange 속성을 추가하여 입력 값을 상태로 관리해야한다.

 

▶️실습 - TodoList컴포넌트 :
TodoBox컴포넌트에서 받은 updateHandler를 Todo컴포넌트에 다시 전달

import Todo from "./Todo";

const TodoList = ({ todoList, deleteHandler, updateHandler }) => {
  return (
    <ul>
      {todoList.map((toDo) => (
        <li key={toDo.id}>
          <Todo
            todo={toDo}
            deleteHandler={deleteHandler}
            updateHandler={updateHandler}
          />
        </li>
      ))}
    </ul>
  );
};

export default TodoList;

  • <Todo todo={toDo} deleteHandler={deleteHandler} updateHandler={updateHandler}
    ➡️TodoBox컴포넌트로부터 받아온 updateHandler를 Todo컴포넌트로 다시 전달

Todo컴포넌트

import { useState } from "react";

const Todo = ({ todo, deleteHandler, updateHandler }) => {
  //...

  const [title, setTitle] = useState(todo.title);

  const updateModeHanlder = () => {
    setUpdateMode(true);
    console.log("updateMode가 " + updateMode + "입니다.");
  };

  // ...

export default Todo;

  • const [title, setTitle] = useState(todo.title);
    ➡️초기값이 todo.title인 이유는 Todo컴포넌트가 TodoList컴포넌트로부터 todo 객체를 입력받아왔으므로
    받아온 todo객체의 title을 수정하겠다고 명시해주어야하므로 todo객체의 title을 초기값으로 설정

 

▶️실습 - Todo컴포넌트 : onChange 적용

const changeHandler = (e) => {
    setTitle(e.target.value);
  };

  if (updateMode) {
    // 수정폼을 활성화시켰을때 (true)
    return (
      <>
        <input type="text" value="수정할 값" onChange={changeHandler} />
        <button>수정</button>
      </>
    );
  }
  • onChange를 이용하여 (e)이벤트가 발생했을때 수행할 changeHandler 이벤트를 요청
    onChange의 코드를 onChange={(e) => setTitle(e.target.value)} 로 바꿔줄 수도 있다.

  • setTitle(e.target.value)
    ➡️changeHandler 함수에서 이벤트가 발생한 객체(e.target)의 값(.value)으로 현재 title을 바꿔주는 코드
    ⚠️setTitle()을 통해 Todo컴포넌트에서만 title이 수정되었기때문에
    TodoBox컴포넌트의 "데이터 배열"까지는 수정된 title이 전달되지 않는다.

▶️실습 - Todo컴포넌트

import { useState } from "react";

const Todo = ({ todo, deleteHandler, updateHandler }) => {
  // 수정폼, 수정
  const [updateMode, setUpdateMode] = useState(false); // 기본값은 false
  // updateMode가 true바뀌면 수정폼이 보여지게끔 구현 가능

  const [title, setTitle] = useState(todo.title);

  const updateModeHanlder = () => {
    setUpdateMode(true);
    console.log("updateMode가 " + updateMode + "입니다.");
  };

  const changeHandler = (e) => {
    setTitle(e.target.value);
  };

  const updateValue = () => {
    updateHandler({ id: todo.id, title: title }); 
    setUpdateMode(false); // 다시 false로 바꿔주어 수정 폼을 비활성화 시킴
  };

  if (updateMode) {
    // 수정폼을 활성화시켰을때 (true)
    return (
      <>
        <input type="text" value="수정할 값" onChagne={changeHandler} />
        <button onClick={updateValue}>수정</button>
      </>
    );
  }

  return (
    <>
      {todo.title}
      <button onClick={() => deleteHandler(todo.id)}>삭제</button>
      <button onClick={updateModeHanlder}>수정</button>
    </>
  );
};

export default Todo;
  • const Todo = ({ todo, deleteHandler, updateHandler})
    ➡️TodoList 컴포넌트로부터 이 속성들을 전달받는다. (이 속성들은 TodoBox컴포넌트로부터 전달되었다)

  • const [title, setTitle] = useState(todo.title);
    ➡️todo객체의 title 속성으로 초기값을 설정해주는데
    이 값은 TodoBox컴포넌트에서 관리되고 있는 리스트의 title속성이 가져와지는 것이다.
    ➡️id값은 primary key (기본키)로 고유한 값을 가지기때문에 title만 바꿔주어 데이터를 관리한다.

  • const updateValue = () ⇒ { ... }
    ➡️활성화된 수정폼 if문안에서 버튼 안에 onClick으로 작성된 이 이벤트핸들러는
    다시 TodoBox컴포넌트 리스트를 수정할 수 있도록 TodoBox컴포넌트까지 수정된 값을 보내는 기능 수행updateHandler() 에 id와 title값을 전달한다. 이 updateHandler()는 TodoBox컴포넌트로부터 전달된 함수이다.
  • setUpdateMode(false);
    ➡️다시 false로 바꿔주는 기능을 하여 수정이 끝나면 수정폼을 다시 닫아주는 기능

▶️실습 - TodoBox컴포넌트 적용

  // 3. 할 일 수정
  const updateTodoList = (todo) => {
    todoList.map((item) => {
      if (item.id === todo.id) {
        item.title = todo.title;
      }
    });
  };
  • 함수의 인자로는 (todo)를 받고 있다. 이는 객체이므로 updateHandler()안에는 객체를 전달하게된다.

 

  • 수정이 잘 되고 있는 것을 확인할 수 있다.
  • 이 후 자바스크립트의 express 서버 등을 활용하여 백엔드와 프론트엔드를 연결하는 것도 가능하다.

Express회고는 별도의 회고로 작성해야겠다!


🚀 회고를 통해 TodoBox컴포넌트로부터 Todo컴포넌트까지의 프로젝트 흐름 등을 깨달을 수 있었고

구조적으로는 이해할 수 있었지만

Todo리스트를 처음부터 다시 설계해보려할때 이 과정을 생각해낼 수 있는지가 관건인것 같다.

비슷한 프로젝트를 진행하여서 익숙하게 만드는 방법 밖에 없을 것 같다!