[맨 땅에 리액트 하기(Feat. 타입스크립트)] todo list 만들기

itisny·2024년 3월 1일
0
post-thumbnail

리액트 경험은 설치만 해봤다싶은 정도고 타입스크립트는 처음봤습니다. 원티드 온보딩 챌린지 참가를 위해 사전 과제를 수행하는 과정이 담긴 글입니다.

요구사항 이해하기

todo list를 만들기 위한 조건은 다음과 같습니다.

  • Add와 Delete 기능 2 가지를 만듭니다.
    • input 창에 list1을 입력 후 Add 버튼을 누르면 input 창 하단에 list1 컴포넌트가 뜹니다.
    • 각 list의 delete 버튼을 누르면 해당 list 컴포넌트가 삭제됩니다.
  • 가능하면 컴포넌트를 만듭니다. (작은 단위라도 좋습니다.)
  • 전역상태를 관리해주세요.(가능한 Redux나 Redux-toolkit을 이용해주세요.)

일단, 추가와 삭제가 가능한 todo list를 만들라는 요구사항을 확인했습니다.

리액트 이해하기

그런데 리액트가 프론트엔드에서 사용하는 프레임워크인 것만 알지, 기본적인 개념을 몰라서 구글링을 해봤습니다.
벨로퍼트님의 글을 봤을때, 이해한 내용은 다음과 같습니다.

  • 요즘 웹은 동적으로 UI를 표현해야해서 처리해야 할 것들이 많은데, 규모가 커질수록 DOM을 직접 건드리면서 작업하면 코드가 난잡해지기 쉽다.
    그래서, 특정 값이 바뀌면 특정 DOM의 속성이 바뀌도록 연결을 해서, 업데이트 하는 작업을 간소화해주는 웹프레임워크가 등장했다.
  • 리액트는 이와 달리 어떠한 상태가 바뀌었을때, 모든걸 다 날려버리고 모든걸 새로 만드는 원리다.
  • 매번 모든 것을 없애고 만들다보면 성능이 안좋아지는데, 이 문제를 해결한 것이 Virtual DOM이다.
    • Virtual DOM은 메모리에 가상으로 존재하는 DOM(즉, 자바스크립트 객체)으로 변경 속도가 빠르다.
    • 업데이트가 필요한 부분만 Virtual DOM으로 수정한다.
    • Virtual DOM과 브라우저 DOM을 비교하여 차이가 있는 곳을 감지한후, 브라우저 DOM에 패치시켜준다.

리액트는 Virtual DOM을 통해 UI 업데이트를 해주는 프레임워크라고 이해했습니다.

node.js 설치하기

이제, 리액트를 깔아봐야겠죠?
리액트를 깔기전 node 설치가 필요하다고 합니다.
저는 이미 깔려있는 상태이기 때문에, 버전 확인만 해보겠습니다.

안내사항 중에, 프로젝트는 node version 20에서 실행할 수 있어야합니다.라는 문구가 있었기 때문에 버전 업데이트를 해보겠습니다.

업데이트를 하기 위해서 npm install -g n 명령어를 통해 n을 설치하면 됩니다. 하지만, 윈도우는 지원이 안돼서 이 명령어를 통해 설치가 되지 않습니다..
그래서 페이지에서 직접 다시 설치하거나, nvm를 직접 깔아서 설정해주면 됩니다.

일단, 빠르게 하기위해서 node.js 페이지에 들어가서 node.js를 다운 받겠습니다.
https://offbyone.tistory.com/441 에서 도움을 받아 설치했습니다.

다시 설치하여, node version 20으로 변경된 것을 확인했습니다.

그리고, 선택적이긴 하지만 좀 더 빠른 속도를 위해 yarn을 설치했습니다.

react 프로젝트 만들기

react 프로젝트를 생성하는 방법이 여러가지인 것 같은데, 저는 기본 create-react-app으로 프로젝트를 생성하겠습니다. 그리고 이 챌린지에서는 타입스크립트를 이용하기 때문에 리액트 생성을 하면서 동시에 타입스크립트를 적용해줘야합니다.

애플코딩님 글 참고
먼저, npm install -g typescript으로 타입스크립트를 설치해줍니다.
tsc -version으로 타입스크립트 버전확인이 가능합니다.
git bash에서 $ npx create-react-app ts-react-todo --template typescript을 실행하면, ts-react-todo폴더가 만들어지면서 react 프로젝트가 생성됩니다.

제대로 실행되는지 확인하기 위해서 $ cd ts-react-todo으로 react-todo 디렉토리로 들어가서 $ yarn start($ npm start)를 실행했습니다.

localhost:3000으로 접속하여 정상적으로 실행되는 것을 확인했습니다.

react 프로젝트 폴더 셋팅하기

프로젝트를 생성하면, 지금 만드려는 todo 프로젝트에 필요하지 않은 파일도 있습니다.
이것을 없애주고 제시해주신 폴더 구조로 셋팅해보겠습니다.

/src
  /components
    /List
  /store
  App.tsx
  index.tsx

todo list 코드 작성하기

기본적으로 react에 코드가 작성되어 있습니다.
일단 App.tsx 파일을 들여다보면 다음과 같은 코드로 작성되어있습니다.

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

이제 이 코드를 타입스크립트 형식으로 작성해주면 될 것 같습니다.

처음 작성해보는 거라 일단 다른 분의 글을 보면서 작성했습니다.
https://medium.com/@dhanushkatharindu99/simple-todo-app-with-react-and-typescript-77da1189dc18

import React from 'react';

function App() {
  return (
    <div className="App">
      
    </div>
  );
}

export default App;

필요없는 코드 부분을 지웠더니 이 형태로 남아 있습니다.

이제 이 코드를 타입스크립트 형식으로 바꿔보겠습니다.
function App(){const App:React.FC = () =>{로 바꿔줍니다.
React.FC는 앱이 기능적 요소라는 것을 알려줍니다.

import React from 'react';

const App:React.FC = () => {
  return (
    <div className="App">
      
    </div>
  );
}

export default App;

먼저, import {useState} from 'react'를 통해 useState를 import해줍니다.
그다음 Todo 유형을 정의하고 useState 후크를 사용하여 상태에 할일을 추가합니다.
id는 todo의 고유 식별번호, task는 할 일의 내용을 확인하는 필드를 넣었습니다.

import React, { useState } from 'react';

type Todo = {
    id: number;
    task: string;
};

function App() {
    const [todos, setTodos] = useState<Todo[]>([]);

    return (
      <div className="App"></div>
    );
}

그리고 하나는 todo를 추가하는 입력창을, 다른 하나는 todo 리스트를 보여줍니다.

const App: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  
  return (
    <div className="App">
      <form>
        <input type="text" name="task" placeholder="할 일을 추가하세요" />
        <button type="submit">Add</button>
      </form>
      <ul className="todoList"></ul>
    </div>
  );
}

export default App;

이제 입력 필드에 전달된 데이터를 가져오기 위해 useState를 사용합니다.
입력이 하나뿐이므로 입력에 대해 별도의 state를 지정해줍니다.
앱 내에서 입력 상태를 감시하는 변수를 선언하고 입력 태그 안에 필드 값을 추가해 보겠습니다.

import React, { useState } from 'react';

const App: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [task, setTask] = useState<string>("");
  
  return (
    <div className="App">
      <form>
        <input type="text" name="task" placeholder="할 일을 추가하세요" value={task} />
        <button type="submit">Add</button>
      </form>
      <ul className="todoList"></ul>
    </div>
  );
}

export default App;

이제 이벤트를 처리할 차례입니다. 사용자가 작성하는 내용을 저장하려 합니다.
입력 필드의 값을 모니터링하고 이를 저장하는 함수 handleChange 만들겠습니다.
이를 위해 HTMLInputElement 타입의 ChangeEvent라는 리액트 내장 함수를 사용하겠습니다.
input에 onChange 속성을 추가하여 handleChange 함수를 추가합니다.

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setTask(event.target.value);
  }
  
  return (
    <div className="App">
      <form>
        <input type="text" name="task" placeholder="할 일을 추가하세요" value={task} onChange={handleChange}/>
        <button type="submit">Add</button>
      </form>
      <ul className="todoList"></ul>
    </div>
  );

이제 사용자가 추가 버튼을 클릭할 때마다 사용할 추가 작업 함수를 만들어 보겠습니다.
form에 onSubmit 속성을 추가하여 addTodo 함수를 추가합니다.
버튼을 눌렀을때 새로고침되는 것을 방지하기 위해 FormEvent라는 리액트 내장 함수를 사용하겠습니다.

  const addTodo = (event: React.FormEvent) => {
    event.preventDefault();

    // check if the value is empty
    if (task.trim().length === 0) {
      alert("값을 넣어주세요!");
      return;
    }

    // create a new todo
    const todo: Todo = {
      id: Date.now(),
      task: task
    };

    // add todo to the state
    setTodos([todo, ...todos]);

    // clear the value of task
    setTask("");
  }
  
  return (
    <div className="App">
      <form onSubmit={addTodo}>
        <input type="text" name="task" placeholder="할 일을 추가하세요" value={task} onChange={handleChange}/>
        <button type="submit">Add</button>
      </form>
      <ul className="todoList">
        {todos.map((todo) => (
          <li key={todo.id}> {todo.task} </li>
        ))}
      </ul>
    </div>
  );

빈 값인 데이터를 넣지 않기위해 공백을 확인하고, todo의 id는 Date.now(), 할 일 내용은 입력한 값을 , 완료여부는 false로 설정하여 상태에 todo를 추가해줍니다.
그리고 task의 값은 다시 공백으로 설정해줍니다.

빈값인 경우 '값을 넣어주세요!'라는 알림창이 뜨고, 추가 될 경우 map을 통해 task가 출력되는 것을 확인할 수 있습니다.

이제 사용자가 todo 삭제 버튼을 누를 때 사용할 삭제 작업 함수를 만들어 보겠습니다.


const deleteTodo = (id: number) => {
  const index = todos.findIndex((todo) => todo.id === id);
  todos.splice(index, 1);
  setTodos([...todos]);
}

<ul className="todoList">
  {todos.map((todo) => (
    <li key={todo.id}>
      {todo.task}
      <button onClick={()=> deleteTodo(todo.id)}>Delete</button>
    </li>
  ))}
</ul>

deleteTodo 함수는 할 일에 부여된 id를 찾아서 todo 인덱스에 있는 값 하나를 삭제합니다. 그리고 다시 todos에 넣어 상태를 업데이트 해줍니다.

추가, 삭제가 가능한 todo 리스트를 만들었습니다!

import React, { useState } from 'react';

type Todo = {
  id: number;
  task: string;
}

const App: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [task, setTask] = useState<string>("");

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setTask(event.target.value);
  }

  const addTodo = (event: React.FormEvent) => {
    event.preventDefault();

    // check if the value is empty
    if (task.trim().length === 0) {
      alert("값을 넣어주세요!");
      return;
    }

    // create a new todo
    const todo: Todo = {
      id: Date.now(),
      task: task,
    };

    // add todo to the state
    setTodos([todo, ...todos]);

    // clear the value of task
    setTask("");
  }
  
  const deleteTodo = (id: number) => {
    const index = todos.findIndex((todo) => todo.id === id);
    todos.splice(index, 1);
    setTodos([...todos]);
  }

  return (
    <div className="App">
      <form onSubmit={addTodo}>
        <input type="text" name="task" placeholder="할 일을 추가하세요" value={task} onChange={handleChange}/>
        <button type="submit">Add</button>
      </form>
      <ul className="todoList">
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.task}
            <button onClick={()=> deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

맨 땅에 헤딩하느라, 아직 컴포넌트를 만들고 전역 상태를 관리하는 부분은 못했습니다.
다음에는 컴포넌트를 만들어 보는 것을 목표로 코드를 수정해보겠습니다.💪

0개의 댓글