React

[React] 리액트로 Todo-List (투두리스트) 만들기

어렵지만 2025. 6. 9. 11:29

리액트란 무엇일까요?

사용자 인터페이스 (UI)를 만들기 위한 자바스크립트 라이브러리 입니다.

 

1. 사용자 인터페이스: 우리가 웹사이트나 앱에서 보는 모든것, 즉 버튼, 입력창, 메뉴, 사진 등 화면에 보이는 모든 요소를 의미합니다.

2. 자바스크립트 라이브러리: '프레임워크'가 아닌 '라이브러리'라는 점이 중요합니다. 프레임워크가 집을 짓는데 필요한 모든 것(설계도, 규칙, 도구)을 제공하는 '풀세트'라면, 라이브러리는 특정 기능(예: 못을 박는 망치)을 쉽게 사용 할 수 있도록 제공하는 '도구'에 가깝습니다.

 

1. 컴포넌트 (Component): 레고 블록처럼 조립하는 개발


리액트는 UI를 '컴포넌트'라는 재사용 가능한 조각들로 나누어 생각합니다.홈페이지를 레고로 만든다고 생각해봅시다.
검색창 레고 블록 🧱
뉴스 헤드라인 레고 블록 📰
로그인 정보 레고 블록 👤
광고 배너 레고 블록 📢


이렇게 각 부분을 독립된 블록으로 만들어두고, 필요할 때마다 가져와 조립하면 하나의 완성된 페이지가 탄생합니다.


이게 왜 좋을까요?
재사용성: 똑같은 모양의 버튼이 100개 필요하다면, '버튼 컴포넌트' 하나만 잘 만들어두고 100번 재사용하면 됩니다.
유지보수: 검색창에 문제가 생기면, 다른 부분은 건드릴 필요 없이 '검색창 컴포넌트'만 찾아서 수정하면 끝입니다. 전체 코드를 헤맬 필요가 없죠.
가독성: 코드가 체계적이고 이해하기 쉬워집니다.

 

2. 선언형 (Declarative): "어떻게"가 아닌 "무엇을" 보여줄지에 집중

명령형 (과거 방식): "코끼리를 냉장고에 넣는 법"을 하나하나 지시하는 방식입니다.
냉장고 문을 연다.
코끼리를 들어 올린다.
코끼리를 냉장고 안에 넣는다.
냉장고 문을 닫는다.
선언형 (리액트 방식): "냉장고 안에 코끼리가 있는 상태"를 선언하는 방식입니다.
"냉장고 안에는 코끼리가 있어야 해!" 라고 선언하면, 그 과정은 리액트가 알아서 처리합니다.

 

리액트에서는 "이 데이터(state)가 'A'라는 값을 가지면, 화면에 이 컴포넌트를 보여줘!" 라고 선언만 하면 됩니다. 'A'라는 값이 'B'로 바뀌었을 때, 기존 컴포넌트를 지우고 새 컴포넌트를 그리는 복잡한 과정은 리액트가 내부적으로 알아서 처리해줍니다.

개발자는 그저 '상태'가 변하면 UI가 어떻게 '보여야 하는지'에만 집중하면 되므로 코드가 훨씬 간결하고 예측 가능해집니다.

 

3.  가상돔 (Virtual DOM): 똑똑한 설계도로 불필요한 공사 막기


웹 브라우저는 DOM(Document Object Model)이라는 '설계도'를 보고 화면을 그립니다. 

 

그런데 이 실제 DOM을 직접 변경하는 작업은 매우 느리고 비용이 많이 듭니다. 집의 벽지 색깔 하나 바꾸려고 집 전체를 허물고 다시 짓는 것과 비슷하죠.
리액트는 이 문제를 해결하기 위해 '가상돔(Virtual DOM)'이라는 것을 사용합니다.
변화 감지: 데이터에 변화가 생기면, 리액트는 실제 DOM을 건드리지 않고 메모리상에 존재하는 '가상의 DOM'을 먼저 업데이트합니다. (마치 진짜 설계도가 아닌, 복사본 설계도에 변경 사항을 먼저 그리는 것과 같습니다.)
차이 비교: 이제 리액트는 이전 버전의 가상돔과 새로운 버전의 가상돔을 비교하여 정확히 어떤 부분이 바뀌었는지 찾아냅니다. 
최소한의 업데이트: 마지막으로, 바뀐 부분만 찾아서 실제 DOM에 딱 한 번 적용합니다.

 

예제코드 

(리액트 설치)
npx create-react-app todo-list

cd todo-list

npm start

 

App.css

body {
  background-color: #f4f4f4;
  font-family: sans-serif;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  margin: 0;
}

.app-container {
  background: white;
  padding: 2rem;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  width: 400px;
}

h1 {
  text-align: center;
  color: #333;
}

.todo-list {
  margin-top: 1rem;
}

.todo-item {
  background: #fff;
  padding: 0.8rem;
  border-bottom: 1px solid #eee;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.input-container {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.input-container input {
  flex-grow: 1;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.input-container button {
  padding: 0.5rem 1rem;
  border: none;
  background-color: #5C6BC0;
  color: white;
  border-radius: 4px;
  cursor: pointer;
}

.input-container button:hover {
  background-color: #3F51B5;
}
.todo-item.completed span {
  text-decoration: line-through;
  color: #aaa;
}

.todo-item span {
  cursor: pointer;
  flex-grow: 1;
}

.delete-button {
  background-color: #EF5350;
  color: white;
  border: none;
  padding: 0.3rem 0.6rem;
  border-radius: 4px;
  cursor: pointer;
  opacity: 0; /* 평소에는 안보이게 */
  transition: opacity 0.2s;
}

.todo-item:hover .delete-button {
  opacity: 1; /* 마우스를 올리면 보이게 */
}

 

App.js

import React, { useState } from 'react';
import './App.css';

function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: '리액트 공부하기', completed: false },
    { id: 2, text: '블로그에 글 올리기', completed: false },
  ]);
  const [input, setInput] = useState('');

  const addTodo = () => {
    if (input.trim() === '') return;
    const newTodo = {
      id: Date.now(),
      text: input,
      completed: false,
    };
    setTodos([...todos, newTodo]);
    setInput('');
  };

  // 5. 완료 상태를 토글(toggle)하는 함수
  const toggleTodo = (id) => {
    setTodos(
      todos.map(todo =>
        // id가 일치하는 항목을 찾으면,
        // completed 값을 반전시킨 '새로운' 객체를 반환합니다.
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  // 6. 할 일을 삭제하는 함수
  const deleteTodo = (id) => {
    // id가 일치하지 않는 항목들만 모아서 '새로운' 배열을 만듭니다.
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div className="app-container">
      <h1>My To-Do List</h1>
      <div className="input-container">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()} // Enter 키로도 추가
          placeholder="새로운 할 일을 입력하세요"
        />
        <button onClick={addTodo}>추가</button>
      </div>

      <div className="todo-list">
        {todos.map(todo => (
          // 7. UI에 기능 연결
          <div
            key={todo.id}
            // completed 상태에 따라 다른 className을 적용
            className={`todo-item ${todo.completed ? 'completed' : ''}`}
          >
            <span onClick={() => toggleTodo(todo.id)}>
              {todo.text}
            </span>
            <button className="delete-button" onClick={() => deleteTodo(todo.id)}>삭제</button>
          </div>
        ))}
      </div>
    </div>
  );
}
export default App;