072DATA

TodoList 만들기 본문

FrontEnd/TypeScript

TodoList 만들기

0720 2024. 9. 26. 19:42

 

오늘은 TypeScript를 학습하는 과정에서 TodoList를 만들었으며 

그 과정을 기록해도록 하겠습니다.

 

프로젝트 세팅

 

TypeSript 프로젝트 생성은 생략하며,

 

먼저 API 요청을 하기위해서 JSON 서버를 Install 합니다

npm install json-server

 

 

설치를 완료한 뒤 root 경로에 db.json 파일을 추가하여 키와 값을 입력합니다, 형식은 아래와 같습니다!

{
  "todos": [
    {
      "id": "1",
      "text": "할 일 1",
      "completed": false
    }
  ]
}

 

 

또 원할한 프로젝트 구현을 위하여 json-server 명령어를 .pakage.json의 scripts에 추가합니다

  "scripts": {
    //...
    "server": "json-server --watch db.json --port 4000"
  },

 

그럼 이제 본격적으로 시작!

 

Todo.ts 생성

Todo.ts을 생성하는데 이 파일의 역할은

타입 정의, API 호출, 함수를 정의하는 역할입니다!

 

import axios from "axios";

export type Todo = {
  id: string;
  text: string;
  completed: boolean;
};

export type ToggleTodo = Omit<Todo, "text">;

export async function getTodoList(): Promise<Todo[]> {
  try {
    const response = await axios.get<Todo[]>("http://localhost:4000/todos");
    return response.data;
  } catch (error) {
    console.error("Todo GET 요청 에러:", error);
    return [];
  }
}

 

 

그 후 TypeScript를 컴파일 해줘야 하는데 그 이유는 다음과 같습니다

 

=>  TypeScript 코드는 브라우저에서 직접 실행할 수 없기 때문

JavaScript로 컴파일해야 합니다 다음 명령어로 컴파일 해줍니다

 

//컴파일
tsc todo.ts
//실행
node todo.js

 

이제 이 파일을 가지고 컴포넌트에서 TodoList를 구현하면 됩니다!

 

컴포넌트 분리

 

컴포넌트는 두개정도 분리가 됩니다!

TodoList.tsx와 TodoItem.tsx로 분리하면 되는데요

 

물론 App.tsx에서 이 컴포넌트를 만들어도 상관은 없지만 

작은 프로젝트를 임하면서도 유지 보수나 코드 가독성

=> 바로 단일 책임 원칙을 잘 숙지하기 위해서 나누어 보았습니다.

 

폴더 구조

src/
  ├── components/
  │   ├── TodoList.tsx
  │   └── TodoItem.tsx
  ├── App.tsx
  └── ...

 

구조는 이정도로 잡아주면 될 것 같습니다.

 

App.tsx

 

먼저 해야할 일을 추가하기 위한 입력 폼을 넣어주고 하위 컴포넌트인 TodoList로 데이터를 넘겨주어야 합니다.

  return (
    <>
      <h1>TODO LIST</h1>
      <input type="text" onChange={handleTextChange} />
      <button onClick={handleAddTodo}>추가하기</button>
      <TodoList
        todoList={todoList}
      />
    </>
  );

 

 

코드를 보면 Text 변경 및 새로운 Todo를 Add하는 함수가 존재하며 todoList라는 props가 존재합니다 

따라서 필요한 상태를 선언하면 됩니다

const [todoList, setTodoList] = useState<Todo[]>([]);
const [text, setText] = useState("");

 

useState옆에 <Todo[]>라는 타입이 지정되어 있는데 이는 앞서 생성했던 Todo.ts에서

정의된 타입입니다 따라서  <Todo[]>는 Todo 타입의 객체 배열이어야 한다는 뜻입니다!

 

그 다음 text 상태를 보면 아무런 타입도 지정되어 있지 않은데

초기값에 빈 문자열 (" ") 을 넣어주면 TypeScript는 해당 상태를 String 으로 타입 추론을 하게 됩니다! 

 

물론 명시적으로 type을 지정해주면 되지만 타입 추론 개념도 가져가고 싶어서 기록해 보았습니다.

 

Todo 추가하기

  const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  const handleAddTodo = async () => {
    const newTodo: Todo = {
      id: crypto.randomUUID(),
      text,
      completed: false,
    };
    
    try {
      const response = await axios.post<Todo>(
        "http://localhost:4000/todos",
        newTodo
      );
      setTodoList((prev) => [...prev, response.data]);
      setText("");
    } catch (error) {
      console.error("Todo 등록 에러:", error);
    }
  };

 

입력된 내용에 따라 text의 값을 변경하는 함수와 Todo를 등록하는 함수를 선언해줍니다.

newTodo도  Todo 타입으로 정의된 객체인 것을 볼 수 있으며

axios.post에서 응답 데이터의 타입도 Todo로 지정해줍니다

 

뭔가 이쯤되니 타입을 지정해주는게 오히려 더 귀찮은 기분이 듭니다..ㅎㅎ

 

type TodoListProps = {
  todoList: Todo[];
  //...
};

const TodoList = ({
  todoList,
  //...
}: TodoListProps) => {

 

그 다음 TodoList 컴포넌트에서 props로 전달받은 todoList마저 Todo 객체의 배열 타입으로 지정해주고 있는데

상태를 선언할 때 Todo[]로 타입을 지정해줬는데 왜 또 여기서 타입을 지정해줘야 하는지 의문이 들었습니다..

 

G선생님께 한 번 여쭈어보니 이러한 답변을 해주셨어요..

 

  • 스코프의 독립성: todoList는 상태에서 정의된 것과 props에서 정의된 것이 서로 다른 스코프에서 사용됩니다. 상태는 해당 컴포넌트 내부에서 관리되고, props는 외부에서 전달됩니다.
  • 타입 안정성: 각 스코프에서 todoList가 어떤 타입인지 명확하게 정의함으로써, 코드의 안정성을 높이고, 잘못된 타입의 데이터가 들어오는 것을 방지할 수 있습니다.
  • 가독성 및 유지보수: 컴포넌트가 어떤 props를 받는지 명확하게 알 수 있어, 코드 가독성이 향상되고 유지보수가 쉬워집니다.

근데 솔직히 이러한 이유가 있다고 해도 굳이란 생각이 드는 건 마찬가지

어쨌든 props로 전달받았기 때문에 그 부모 컴포넌트의 state 타입만 숙지하면 되는 것 아닌가 싶네욥...ㅠ

 

Todo 삭제 및 업데이트

  const handleDeleteTodo = async (id: Todo["id"]) => {
    try {
      await axios.delete(`http://localhost:4000/todos/${id}`);
      setTodoList((prev) => prev.filter((todo) => todo.id !== id));
    } catch (error) {
      console.log("Todo 삭제 에러:", error);
    }
  };

  const handleUpdateTodo = async ({ id, completed }: ToggleTodo) => {
    try {
      console.log("눌림");
      await axios.patch(`http://localhost:4000/todos/${id}`, {
        completed: !completed,
      });
      setTodoList((prev) =>
        prev.map((todo) =>
          todo.id === id ? { ...todo, completed: !completed } : todo
        )
      );
    } catch (error) {
      console.log("Todo 수정 에러:", error);
    }
  };

 

 

여기서 인자로 받는 id 값을 Todo 객체의 id의 타입으로 지정해주는 삭제 함수와

id, completed 두개의 인자를 받는 함수인 업데이트 함수가 있는데 

여기서 핵심은 `text` 속성 빼고 나머지를 다 받아오고 있는 업데이트 함수입니다.

 

여기서 유틸리티 타입중 하나인 Omit을 사용하여 유연하게 타입을 지정해줄 수 있어요

따라서 ToggleTodo는 Todo.ts에 있는 제네릭 타입을 사용한 타입 별칭이었으며 바로 임포트해서 사용합니다!!

 

props 타입 지정

//TodoList.tsx
type TodoListProps = {
  todoList: Todo[];
  onDeleteClick: (id: Todo["id"]) => void;
  onToggleClick: (toggleTodo: ToggleTodo) => void;
};

const TodoList = ({
  todoList,
  onDeleteClick,
  onToggleClick,
}: TodoListProps) => {
  return (
//....

//TodoItem.tsx
type TodoItemProps = Todo & {
  onDeleteClick: (id: Todo["id"]) => void;
  onToggleClick: (toggleTodo: ToggleTodo) => void;
};

const TodoItem = ({
  id,
  text,
  completed,
  onDeleteClick,
  onToggleClick,
}: TodoItemProps) => {
//.....

 

그리고 컴포넌트로 내려줄 떄 똑같이 타입을 지정해줍니다..왜 굳이 또 지정하냐고

 

그후 작스 문법에서 todoList를 map으로 돌려 데이터를 뿌려주고

필요한 부분에 props로 내려준 함수를 사용해주면 TodoList가 완성됩니다!!

 

결과물

 

간단하쥬 ??ㅎㅎ

 

 

마치며

내 생각보다 훨씬 더 복잡한 타입스크립트... 솔직히 개인적으로 비효율 적인 부분도 있는 것 같음

가독성이 그리 좋지도 않은 것 같고 근데 분명 자바스크립트를 더 안정적으로 사용할 수는 있을 것 같아서

여러가지 라이브러리를 활용하면 확실히 더 쉽고 든든한 언어일 것 같긴 함 낼도 홧팅..

'FrontEnd > TypeScript' 카테고리의 다른 글

모달창 만들기(리액트, TypeScript)  (0) 2024.10.10
TypeScript 타입 선언하는 방법  (0) 2024.09.25
타입스크립트 요약  (0) 2024.09.24