React Typescript 찍먹하기

김 주현·2023년 7월 14일
0

어쩌다 Typescript?

예전에 VB, C++, Java 등등 변수 타입을 지정해가면서 코딩했던 나로선 타입을 지정하지 않아도 되는 Javascript를 보며 이게 무슨 ... 조잡함이지 싶었다. 그렇지만 점차 쓰게 되니까 타입 캐스팅에 연연하지 않게 되었지만, 여전히 요 녀석은 이것이야! 라는 명명을 해주고 싶은 욕망은 계속 있었던 것 같다.

그렇게 찾은 게 처음에는 JSDoc이었다. 물론 Typescript에 대한 존재는 알고 있었지만, 따로 Transpiling을 해줘야 한다는 사실이 날 망설이게 했다. 그래놓고 SCSS는 잘 썼던 게 웃기긴 한데 그래서 간단한 주석처리와 플러그인만으로도 쉽게 타입지정할 수 있는 JSDoc을 찾았다.

물론 모든 변수에 다 쓴 건 아니고,, 쓰다보니 오로지 Intellisense만을 위해 주석을 적긴 했다(...) 간혹 함수나 객체를 쓰는데 어떤 인자를 가지는지, 어떤 메서드를 가졌는지, 어떤 속성을 가졌는지 보여주질 않아서 되게 불편했던 경험이 있었다. 이럴 때만 살짝쿵 타입 지정해주긴 했는데, 점점 이 불편함이 크게 느껴지면서 이왕 이렇게 된 거 Typescript로 아예 넘어가자! 싶어서 이렇게 찍먹을 해본다.

기본적인 내용은 이미 훑었고, 간단하게 투두리스트 만들어보면서 익히고자 한다.

React Typescript 프로젝트 설치

아래 명령어를 입력후 Typescript + SWC 선택 후 설치하면 된다.

npm create vite@latest

그 후 npm install을 통해 패키지들을 설치해주고 구동하면 된다.

(Note) 간혹 설치 직후 사방팔방에서 TS 구문 오류가 뿜어져나오는 경우가 있는데, 그럴 땐 그냥 VSCode를 재시작해주면 된다.

(Note2) 이상하게 yarn으로 설치하면 얘가 정신을 못차리더라..

(Note3) yarn으로 설치해도 되는데, 나는 VSCode가 문제였더라. 프로그램 자체가 User와 System으로 나뉘어져 있는 걸 처음 알았네,,, System은 계속 최신 버전으로 업데이트됐었는데 User는 처음 설치한 그대로의 버전이라 계속 충돌이 일어났던 것 같다. 열받네

패키지 설치 및 설정

Tailwind

역시나 그래도 예쁘면 좋으니까 Tailwind로 슉슉 해주자.

yarn add tailwindcss postcss autoprefixer
touch .prettierrc
.prettierrc
{
  "plugins": ["prettier-plugin-tailwindcss"]
}
.eslintrc.cjs
plugins: ['react-refresh', 'prettier'],
index.css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

@tailwind base;
@tailwind components;
@tailwind utilities;

uuid

고유 ID을 생성해주기 위해 uuid 패키지를 설치하자. 참고로 Typescript로 쓰려면 @types/uuid도 설치해주어야 한다. 쓸 때는 그냥 uuid에서 불러오면 된다.

yarn add uuid @types/uuid

디자인

전체적인 디자인

왼쪽은 할일을 추가하지 않았을 때, 오른쪽은 투두가 좀 추가되었을 때.

수정

삭제

수정과 삭제는 가볍게 Prompt와 Confirm으로 처리했다(ㅋㅋ)

디자인 코드

할일 추가하는 부분

<form className="mb-4 flex h-10 overflow-hidden rounded-lg border border-gray-200">
  <input
    className=" h-full w-full appearance-none bg-none px-2 py-1 outline-none"
    placeholder="할일을 적어보아요✨"
    value={todoInput}
    onChange={(e) => setTodoInput(e.target.value)}
  />
  <button
    className=" h-ull min-w-fit appearance-none rounded-sm bg-gray-100 bg-none px-4 py-1 font-bold uppercase outline-none"
    type="submit"
  >
    추가
  </button>
</form>

할일목록 보여주는 부분_할일 없을 때

<table className=" w-full table-fixed">
  <thead className=" border-t border-gray-100 bg-gray-50">
    <th scope="col" className=" w-1/6 py-4">
      완료
    </th>
    <th scope="col" className=" w-1/2 py-4 text-left">
      할일
    </th>
    <th scope="col" className=" w-1/6 py-4">
      수정
    </th>
    <th scope="col" className=" w-1/6 py-4">
      삭제
    </th>
  </thead>
  <tbody>
    <tr className=" border-b border-gray-100 text-center">
      <td className=" py-3 text-sm text-gray-600" colSpan={4}>
        첫 할일을 추가해보세요🥳
      </td>
    </tr>
  </tbody>
</table>;

할일목록 보여주는 부분_할일 있을 때

<table className=" w-full table-fixed">
  <thead className=" border-t border-gray-100 bg-gray-50">
    <th scope="col" className=" w-1/6 py-4">
      완료
    </th>
    <th scope="col" className=" w-1/2 py-4 text-left">
      할일
    </th>
    <th scope="col" className=" w-1/6 py-4">
      수정
    </th>
    <th scope="col" className=" w-1/6 py-4">
      삭제
    </th>
  </thead>
  <tbody>
    <tr className="border-b border-gray-100">
      <td className=" py-3 text-center">O</td>
      <td className=" py-3">안녕안녕</td>
      <td className=" py-3 text-center">
        <button className="w-fit rounded-lg bg-gray-100 px-4 py-1 font-bold">
          📝
        </button>
      </td>
      <td className=" py-3 text-center">
        <button className="w-fit rounded-lg bg-gray-100 px-4 py-1 font-bold"></button>
      </td>
    </tr>
  </tbody>
</table>;

Type 지정 전

먼저 타입지정을 하지 않고 쓱 만들어본 다음, 차례대로 타입을 지정해볼까 싶다. App 컴포넌트에서 Todos State를 관리하고, Todos Action 역시 App 컴포넌트에서 일단 싹다 구현해보자.

Todo State 생성

todos라는 이름으로 state를 하나 생성해준다. 이 todos에는 id, completed, title를 가진 객체가 들어가게 된다.

const App = () => {
  const [todos, setTodos] = useState([]);
};

Todo 목록 표출

todos에 들어간 할일이 없으면 추가하라는 문구를 띄우고, 있으면 목록을 표출한다.

const App = () => {
  return (
    ...
      <tbody>
        {todos.length === 0 ? (
          <tr className=" border-b border-gray-100 text-center">
            <td className=" py-3 text-sm text-gray-600" colSpan={4}>
              첫 할일을 추가해보세요🥳
            </td>
          </tr>
        ) : (
          todos.map(({ id, completed, title }) => (
            <tr className="border-b border-gray-100" data-todo-id={id}>
              <td className=" py-3 text-center">{completed ? "O" : "X"}</td>
              <td className=" py-3">
                {title}
              </td>
              <td className=" py-3 text-center">
                <button className="w-fit rounded-lg bg-gray-100 px-4 py-1 font-bold">
                  📝
                </button>
              </td>
              <td className=" py-3 text-center">
                <button className="w-fit rounded-lg bg-gray-100 px-4 py-1 font-bold"></button>
              </td>
            </tr>
          ))
        )}
      </tbody>
    ...
  );
}

이벤트 연결

각 필요한 이벤트를 연결해주자.

  • Form: Submit Event Handling
  • Input: Change Event Handling
  • Button: Click Event Handling(수정, 제거)
  • Title: Toggle Event Handling(할일 제목을 누르면 completed 토글)
const App = () => {
  const [todoInput, setTodoInput] = useState('');
  const [todos, setTodos] = useState([]);

  const addTodo = (title) => {};
  const changeTodo = (todoId) => {};
  const toggleTodoCompleted = (todoId) => {};
  const deleteTodo = (todoId) => {};

  const handleSubmit = (event) => {};
  
  return (
    <div className="p-4">
      <form className="mb-4 flex h-10 overflow-hidden rounded-lg border border-gray-200"
        onSubmit={handleSubmit}>
        <input
          className=" h-full w-full appearance-none bg-none px-2 py-1 outline-none"
          placeholder="할일을 적어보아요✨"
          value={todoInput}
          onChange={(e) => setTodoInput(e.target.value)}
        />
        <button
          className=" h-ull min-w-fit appearance-none rounded-sm bg-gray-100 bg-none px-4 py-1 font-bold uppercase outline-none"
          type="submit"
        >
          추가
        </button>
      </form>

      // ... 중략

          todos.map(({ id, completed, title }) => (
            <tr className="border-b border-gray-100" data-todo-id={id}>
              <td className=" py-3 text-center">{completed ? "O" : "X"}</td>
              <td className=" py-3" onClick={() => toggleTodoCompleted(id)}>
                {title}
              </td>
              <td className=" py-3 text-center">
                <button
                  className="w-fit rounded-lg bg-gray-100 px-4 py-1 font-bold"
                  onClick={() => changeTodo(id)}
                >
                  📝
                </button>
              </td>
              <td className=" py-3 text-center">
                <button
                  className="w-fit rounded-lg bg-gray-100 px-4 py-1 font-bold"
                  onClick={() => deleteTodo(id)}
                ></button>
              </td>
            </tr>
          ))
    </div>
  );
};

이벤트 구현

이제 각각의 이벤트에 로직을 작성해보자. 각 로직에 대한 설명은 중요한 건 아니니까 패스!

addTodo

const addTodo = (title) => {
  const newTodo = {
    id: uuid(),
    completed: false,
    title,
  };

  setTodos((prevState) => [...prevState, newTodo]);
};

changeTodo

const changeTodo = (todoId) => {
  const todoIndex = todos.findIndex(({ id }) => id === todoId);

  const newTodoTitle = prompt('새로 바꿀 내용?', todos[todoIndex].title);

  if (!newTodoTitle) {
    return;
  }

  todos[todoIndex] = {
    ...todos[todoIndex],
    title: newTodoTitle,
  };

  setTodos([...todos]);
};

toggleTodoCompleted

const toggleTodoCompleted = (todoId) => {
  const todoIndex = todos.findIndex(({ id }) => id === todoId);

  todos[todoIndex] = {
    ...todos[todoIndex],
    completed: !todos[todoIndex].completed,
  };

  setTodos([...todos]);
};

deleteTodo

const deleteTodo = (todoId) => {
  const todoIndex = todos.findIndex(({ id }) => id === todoId);
  const isConfirm = confirm(`진짜 삭제하실 거에요?\n할일 내용: ${todos[todoIndex].title}`);

  if (!isConfirm) {
    return;
  }

  setTodos([...todos.filter(({ id }) => id !== todoId)]);
};

handleSubmit

const handleSubmit = (event) => {
  event.preventDefault();
  
  if (todoInput === '') {
    return;
  }

  addTodo(todoInput);
  setTodoInput('');
};

여기까지 작성했다면 잘 굴러가는 걸 확인할 수 있다.

Type 지정 전 풀코드

import { useState } from 'react';
import { v4 as uuid } from 'uuid';

const App = () => {
  const [todoInput, setTodoInput] = useState('');
  const [todos, setTodos] = useState([]);

  const addTodo = (title) => {
    const newTodo = {
      id: uuid(),
      completed: false,
      title,
    };

    setTodos((prevState) => [...prevState, newTodo]);
  };

  const changeTodo = (todoId) => {
    const todoIndex = todos.findIndex(({ id }) => id === todoId);

    const newTodoTitle = prompt('새로 바꿀 내용?', todos[todoIndex].title);

    if (!newTodoTitle) {
      return;
    }

    todos[todoIndex] = {
      ...todos[todoIndex],
      title: newTodoTitle,
    };

    setTodos([...todos]);
  };

  const toggleTodoCompleted = (todoId) => {
    const todoIndex = todos.findIndex(({ id }) => id === todoId);

    todos[todoIndex] = {
      ...todos[todoIndex],
      completed: !todos[todoIndex].completed,
    };

    setTodos([...todos]);
  };

  const deleteTodo = (todoId) => {
    const todoIndex = todos.findIndex(({ id }) => id === todoId);
    const isConfirm = confirm(`진짜 삭제하실 거에요?\n할일 내용: ${todos[todoIndex].title}`);

    if (!isConfirm) {
      return;
    }

    setTodos([...todos.filter(({ id }) => id !== todoId)]);
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    if (todoInput === '') {
      return;
    }

    addTodo(todoInput);
    setTodoInput('');
  };

  return (
    <div className="p-4">
      <form
        className="mb-4 flex h-10 overflow-hidden rounded-lg border border-gray-200"
        onSubmit={handleSubmit}
      >
        <input
          className=" h-full w-full appearance-none bg-none px-2 py-1 outline-none"
          value={todoInput}
          placeholder="할일을 적어보아요✨"
          onChange={(e) => setTodoInput(e.target.value)}
        />
        <button
          className=" h-ull min-w-fit appearance-none rounded-sm bg-gray-100 bg-none px-4 py-1 font-bold uppercase outline-none"
          type="submit"
        >
          추가
        </button>
      </form>
      <table className=" w-full table-fixed">
        <thead className=" border-t border-gray-100 bg-gray-50">
          <th scope="col" className=" w-1/6 py-4">
            완료
          </th>
          <th scope="col" className=" w-1/2 py-4 text-left">
            할일
          </th>
          <th scope="col" className=" w-1/6 py-4">
            수정
          </th>
          <th scope="col" className=" w-1/6 py-4">
            삭제
          </th>
        </thead>
        <tbody>
          {todos.length === 0 ? (
            <tr className=" border-b border-gray-100 text-center">
              <td className=" py-3 text-sm text-gray-600" colSpan={4}>
                첫 할일을 추가해보세요🥳
              </td>
            </tr>
          ) : (
            todos.map(({ id, completed, title }) => (
              <tr className="border-b border-gray-100" data-todo-id={id}>
                <td className=" py-3 text-center">{completed ? "O" : "X"}</td>
                <td className=" py-3" onClick={() => toggleTodoCompleted(id)}>
                  {title}
                </td>
                <td className=" py-3 text-center">
                  <button
                    className="w-fit rounded-lg bg-gray-100 px-4 py-1 font-bold"
                    onClick={() => changeTodo(id)}
                  >
                    📝
                  </button>
                </td>
                <td className=" py-3 text-center">
                  <button
                    className="w-fit rounded-lg bg-gray-100 px-4 py-1 font-bold"
                    onClick={() => deleteTodo(id)}
                  ></button>
                </td>
              </tr>
            ))
          )}
        </tbody>
      </table>
    </div>
  );
};

export default App;

컴포넌트 분리

이제 데이터와 UI를 분리하기 위해 컴포넌트로 분리해보자. 크게 분리할 부분은 '입력' 부분과 '표출' 부분이다. 할일을 추가하는 부분을 TodoForm, 할일을 보여주는 부분을 TodoTable이라고 하겠다. Components 폴더를 만들고 파일을 만들어주자.

TodoForm.tsx

input에 연결된 state를 여기로 옮기고, Submit Event때 넘겨받은 onSubmit으로 값만 올려준다.

import { useState } from "react";

const TodoForm = ({ onSubmit }) => {
  const [todoInput, setTodoInput] = useState("");

  const handleSubmit = (event) => {
    event.preventDefault();

    onSubmit(todoInput);
    setTodoInput("");
  };

  return (
    <form
      className="mb-4 flex h-10 overflow-hidden rounded-lg border border-gray-200"
      onSubmit={handleSubmit}
    >
      <input
        className=" h-full w-full appearance-none bg-none px-2 py-1 outline-none"
        placeholder="할일을 적어보아요✨"
        value={todoInput}
        onChange={(e) => setTodoInput(e.target.value)}
      />
      <button
        className=" h-ull min-w-fit appearance-none rounded-sm bg-gray-100 bg-none px-4 py-1 font-bold uppercase outline-none"
        type="submit"
      >
        추가
      </button>
    </form>
  );
};

export default TodoForm;

TodoTable.tsx

목록 표출에 필요한 todos를 넘겨주고, 이 todos에 대한 action들은 props로 넘겨받는다. 각 action이 필요한 이벤트에서 호출시켜준다.

const TodoTable = ({ todos, onToggle, onChange, onDelete }) => {
  return (
    <table className=" w-full table-fixed">
      <thead className=" border-t border-gray-100 bg-gray-50">
        <th scope="col" className=" w-1/6 py-4">
          완료
        </th>
        <th scope="col" className=" w-1/2 py-4 text-left">
          할일
        </th>
        <th scope="col" className=" w-1/6 py-4">
          수정
        </th>
        <th scope="col" className=" w-1/6 py-4">
          삭제
        </th>
      </thead>
      <tbody>
        {todos.length === 0 ? (
          <tr className=" border-b border-gray-100 text-center">
            <td className=" py-3 text-sm text-gray-600" colSpan={4}>
              첫 할일을 추가해보세요🥳
            </td>
          </tr>
        ) : (
          todos.map(({ id, completed, title }) => (
            <tr className="border-b border-gray-100" data-todo-id={id}>
              <td className=" py-3 text-center">{completed ? "O" : "X"}</td>
              <td className=" py-3" onClick={() => onToggle(id)}>
                {title}
              </td>
              <td className=" py-3 text-center">
                <button
                  className="w-fit rounded-lg bg-gray-100 px-4 py-1 font-bold"
                  onClick={() => onChange(id)}
                >
                  📝
                </button>
              </td>
              <td className=" py-3 text-center">
                <button
                  className="w-fit rounded-lg bg-gray-100 px-4 py-1 font-bold"
                  onClick={() => onDelete(id)}
                ></button>
              </td>
            </tr>
          ))
        )}
      </tbody>
    </table>
  );
};

export default TodoTable;

App.tsx

그러면~ 최종적으로 App 코드는 다음과 같이 된다. App에서는 실질적인 State의 Action 로직들이 구현되고, TodoForm에선 입력을, TodoTable엔 계산된 Todos와 이를 조작할 수 있는 Action들을 넘겨준다.

import { useState } from 'react';
import { v4 as uuid } from 'uuid';

import TodoForm from './Components/TodoForm';
import TodoTable from './Components/TodoTable';

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

  const addTodo = (title) => { ... };
  const changeTodo = (todoId) => { ... };
  const toggleTodoCompleted = (todoId) => { ... };
  const deleteTodo = (todoId) => { ... };
  const handleSubmit = (todoInput) => { ... };

  return (
    <div className="p-4">
      <TodoForm onSubmit={handleSubmit} />
      <TodoTable
        todos={todos}
        onToggle={toggleTodoCompleted}
        onChange={changeTodo}
        onDelete={deleteTodo}
      />
    </div>
  );
};

export default App;

Typescript 기본

컴포넌트 분리까지 끝났으니, 본격적으로 Type을 지정하기 전에 Typescript를 슬쩍 훑고 가자.

변수 타입 지정

변수에 타입을 지정할 땐 :를 이용해서 타입을 일러주면 된다. 간단하죠? Typescript의 원시타입들은 다 소문자이니까 확인확인!

const myText: string = ''
const myNumber: number = 10

배열 타입 지정

배열은 위의 변수 타입에 []를 붙여주면 된다.

const myTexts: string[] = ['안녕안녕', '그래 안녕하다']
const myNumbers: number[] = [0, 1, 2]

객체 타입 지정

객체는 key들을 미리 지정하고, 그 키들에 대한 타입을 지정해주어야 한다.

const myPerson: { name: string, age: number } = {
  name: "김 주핸",
  age: 25
}

객체 배열은?

그러면 객체 배열은 어떻게 쓸까? 마찬가지로 뒤에 []를 붙여주면 된다.

const myPeople = { name: string, age: number}[] = [
  {
    name: "김 주핸",
    age: 25
  },
  {
    name: "김 주현"
    age: 27
  }
]

Custom Type

그런데 이렇게 일일이 다 써주다간 퇴사가 마려울 수도 있다. 그럴 때 직접 Type을 만드는 방법이 존재한다.

type Person = {
  name: string;
  age: number;
}

type People = Person[]

const myPerson: Person = { name: "김 주현", age: 25 }
const myPersons: Person[] = [ ~ ];
const myPeople: People = [ ~ ];

Jesus Christ,, 코드가 굉장히 예뻐졌다.

Optional Type

지금은 나이를 기입하고 있지만, 어떤 사람들은 나이를 밝히고 싶지 않을 수도 있다. 그러면 값을 안 적어낼 테고, 값이 없으니 undefined가 될 것이다. 그러나, age의 type을 number로 지정해두었으니 undefined는 들어올 수 없다. 이럴 경우엔? ?이라는 걸 쓰면 된다.

type Person = {
  name: string;
  age?: number; // 쓸 수도 있고 안 쓸 수도 있다
}

const lady: Person = { name: "숙녀" }

그러면 숙녀의 나이는 비밀이 된다.

Union

아니면 특정 세대라도 받을 수 있을 것이다. 10대, 20대, 30대 이런 식으로. 그럴 때 쓰는 것은 |이다. 요걸 사용하게 되면 Intellisense에도 나타나게 된다.

type Person = {
  name: string;
  age: '10대' | '20대' | '30대' | '서울과기대'
}

literal type

그래도 좀 더 구분짓기 위해서 초반, 중반, 후반으로 더 나눈다고 생각해보자. 그럼 아래와 같이 작성할 수도 있다.

type Person = {
  name: string;
  age: '10대 초반' | '10대 중반' | ...

하지만 너무 노가다가 아닌가! 이 역시 퇴사가 마려울 수 있다. 이럴 경우엔 다음와 같이도 타입 지정을 할 수 있다.

type AgeA = '10대' | '20대' | '30대' | '서울과기대'
type AgeB = '초반' | '중반' | '후반'
type AgeGroup = `${AgeA} ${AgeB}`

type Person = {
    name: string;
    age: AgeGroup;
}

Exclude

사실 나이에 '서울과기대 초반', '서울과기대 중반', '서울과기대 후반'이라는 것은 없다. 이럴 경우 특정 타입만 제거할 수도 있는데, 그것이 Exclude 되시겠다.

type AgeA = '10대' | '20대' | '30대' | '서울과기대'
type AgeB = '초반' | '중반' | '후반'
type AgeGroup = Exclude<`${AgeA} ${AgeB}`, `서울과기대 ${AgeB}`> | "서울과기대"

type Person = {
  name: string;
  age: AgeGroup;
}

ㅋㅋ

Generic Type

아참, Exclude를 쓸 때 <>라는 표현을 썼는데 이게 바로 Generic Type이라는 거다. 이 Generic Type은 나중에 사용자가 타입을 지정해서 쓸 수 있게끔 하는, 다시 말해서 현재 어떤 타입 값이 들어오긴 할 텐데, 어떤 타입인진 나중에 정하겠다는 뜻이다. 일단 그 나중에 들어올 타입을 지칭하는 것이 Generic Type이고, <>를 써서 표현한다.

type Person<T> = { // 나중에 들어올 타입을 T라고 지칭한다.
  name: string;
  age: AgeGroup;
  personality: T; // personality는 T타입
}

// personality를 Mad | Happy | Serious 타입으로 설정 --> T에는 이게 들어가게 됨
type PersonWithPersonality = Person<'Mad' | 'Happy' | 'Serious'>

T 대신에 다른 걸 써도 되는데, 보통 일반적으로 T라고 지칭한다. 국룰

Function Type

함수 역시 타입을 지정해줄 수 있다. 인자에는 어떤 게 들어가는지, 반환값은 어떤 타입인지 지정해준다.

type SayFunction = (saying: string) => boolean

type Person = {
  name: string;
  age: AgeGroup;
  say: SayFunction
}

const personSay: SayFunction = (saying) => {
  const isMadeNoise: boolean = trySay(saying);
  return isMadeNoise;
}

const myPerson: Person = {
  name: "조커",
  age: "서울과기대",
  say: personSay
}

타입 추론

마지막으로 타입 추론에 대해서 알아보겠다. 사실 타입스크립트라고 해서 모든 변수와 함수에 타입을 명시해주어야 하는 건 아니다. 되도록 해주면 좋지만, 들어가는 값이나 반환되는 값이 예상이 가는 경우라면 명시해주지 않아도 자동으로 타입 추론이 된다.

type MyFunc = (param: string) => boolean

const myFunction: MyFunc = (param) => {
  return param === 'somthing' ? true : false;
}

const result = myFunction(a);
const myA = 'hihi'

이런 식으로 MyFunc type에 인자는 string, 반환값은 boolean으로 지정해놨을 때, myFunction의 인자 param엔 따로 string이라고 명시하지 않아도 string 타입을 가지게 되며, 이 함수의 반환값을 가진 result는 따로 명시하지 않아도 boolean 타입을 가지게 된다.

또한, myA는 string이라고 따로 명시하지 않았지만, 'hihi'라는 원시 문자열을 받았기 때문에 myA는 string으로 타입 추론이 진행된다.

알아야 할 점은, typscript는 정적 타이핑 언어니까 string으로 타입 추론이 된 이후, 다른 타입으로 값을 지정한다면 오류를 일으킨다는 점이다. 이를 주의하자.

React Typescript

기본도 짚어보았으니, 이제 컴포넌트 분리를 완료한 것에서 타입까지 붙여보자.

Todo 타입 선언

먼저 제일 기본 타입이 되는 Todo에 대한 타입을 지정해주자.

type Todo = {
  id: string;
  completed: boolean;
  title: string;
};

type TodoList = Todo[];

id, completed, title를 가지고 있는 객체를 Todo 타입이라고 선언했다. 또, 이 타입을 여러개 가지는 경우 TodoList라는 타입으로 정의했다.

만약 여기에서 title에 여러 타입들이 들어간다고 해보자. 그러면 generic하게 다음과 같이 쓸 수 있을 것이다.

type Todo<T> = {
  id: string;
  completed: boolean;
  title: T
}

type TodoList<T> = Todo<T>[];

아니면 들어가는 타입들이 몇개 정해져있다면 extends를 활용해서 다음과 같이도 될 것.

type TodoList<T extends string | number | object> = Todo<T>[];

그러면 TodoList<string>, TodoList<number>, TodoList<object>로 타입을 쓸 수 있으며, 만약 허용되지 않은 타입인 TodoList<boolean>을 쓰게 된다면 오류가 난다.

타입 파일 구분

또, 이 Todo라는 타입은 프로젝트 전체에서 두루두루 쓰일 예정이니 따로 파일로 빼두어 관리해주는 것이 좋을 것이다. Types라는 폴더를 만들고 Todo.types.ts라는 이름으로 파일을 만들자.

그 후, 만들어둔 타입들을 export해주자.

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

export type TodoList = Todo[];

그러면 이제 Todo 타입이 필요한 녀석들은 Types/Todo.types.ts에서 불러오면 된다.

todos State에 지정

App.tsx에서 관리해주고 있는 todos State에 타입을 지정해주자.

import { Todo, TodoList } from './Types/Todo.types';

const [todos, setTodos] = useState<TodoList | []>([]);

Todo.types에서 TodoTodoList를 불러왔고, useState에 해당 State는 TodoList 타입이 들어올 것임을 명시하고 있다. 여기에서, 초기값을 []으로 주었는데, 그러면 자동으로 useState는 해당 state가 [] 타입이라고 추론하게 된다.

하지만 이 []TodoList는 같은 값일지라도 다른 타입임을 명심하자.

그러므로 TodoList 타입을 따로 지정해주지 않으면 에러가 나니까 Union을 통해 TodoList도 지정해주었다. 그러면 todos의 타입은 TodoList | []가 된다.

useState의 Dispatch Type

잠깐 setTodos에 대해서 살펴보면, useState의 반환값으로 나온 녀석이고, useState 내부에서는 이에 대한 반환값을 이미 타입을 지정해서 넘겨주고 있기 때문에 setTodos는 React.Dispatch<React.SetStateAction<TodoList | []>> 라는 타입을 가지고 있다.

addTodo

다음으로 Todo를 추가할 때는 어떻게 해야할까?

  const addTodo = (title: string) => {
    const newTodo: Todo = {
      id: uuid(),
      completed: false,
      title,
    };

    setTodos((prevState) => [...prevState, newTodo]);
  };

어렵지 않게, Todo라는 타입을 지정해준 다음 넣어주면 된다.

각 이벤트 인자 타입

나머지 이벤트들의 인자에 넘어오는 것들도 다 타입 지정을 해주면 된다.

  const handleSubmit = (todoInput: string) => { ... }
  const toggleTodoCompleted = (todoId: string) => { ... }
  const changeTodo = (todoId: string) => { ... }
  const deleteTodo = (todoId: string) => { ... }

컴포넌트 Prop 타입

컴포넌트에 Prop을 넘겨줄 때, 이 Prop에 대한 타입은 어떻게 지정해야 할까? 뭔가 다르게 할 것 같지만, 여전히 해온대로 타입을 지정해주면 된다.

// TodoForm.tsx

type TodoFormProps = {
  onSubmit: (todoInput: string) => void;
};

const TodoForm = ({ onSubmit }: TodoFormProps) => { ... }
// TodoTable.tsx

type TodoTableProp = {
  todos: TodoList;
  onToggle: (todoId: string) => void;
  onChange: (todoId: string) => void;
  onDelete: (todoId: string) => void;
};

const TodoTable = ({ todos, onToggle, onChange, onDelete }: TodoTableProp) => { ... }

여기에서 좀 더 캡슐화 & 일반화시켜주면 특정 인자로 하는 이벤트 타입을 정의해줄 수 있을 것이다.

type EventFunc<T extends string | something> = (param: T) => void
type StringEvent = EventFunc<string>
type SomethingEvent = EventFunc<something>

type TodoTableProp = {
  onToggle: StringEvent,
  onChange: StringEvent;
  onDelete: StringEvent;
};

이러면 위에서 적었던 handle 이벤트들에게도 써먹을 수 있다.

  const handleSubmit: StringEvent  = (todoInput) => { ... }
  const toggleTodoCompleted: StringEvent = (todoId) => { ... }
  const changeTodo: StringEvent = (todoId) => { ... }
  const deleteTodo: StringEvent = (todoId) => { ... }

멋진 Typescript!

HTML Element, Event 타입

마지막으로, 각 HTML Element에서 발생하는 Event들의 타입에 대해서 알아보자. 이 HTML Element들과 Event들은 React에서 따로 타입을 지정해두었다.

예를 들어, Input Element라면 HTMLInputElement라는 타입을, Button Element라면 HTMLButtonElement 타입을 가진다.

대충 뭐 이런 ... HTML의 모든 Element를 정의해놨다고 생각하면 된다. 해당 객체를 가져와서 쓰면 되고, Event 역시 다 정의를 해두었다.

잘 보면, 각 이벤트들은 Element라는 Type을 가지고 있다. 이게 무슨 말이냐? 이 이벤트들은 HTMLElement 타입을 받는다는 것이다.

예를 들어, Form객체의 onSubmit 이벤트 타입을 받고 싶다면 다음과 같은 형태로 이루어질 것이다. 어째 점점 딥해진다....

// type React.FormEventHandler<T = Element> = (event: FormEvent<T>) => void
const handleSubmit: FormEventHandler<HTMLFormElement> = (event: FormEvent<HTMLFormElement>) => { ... }

그러면 매번! 이런 이벤트들을 찾아보고 정의해주어야 하는 걸까? 머..머.. 절반은 맞는 말이다. VSCode는 이런 객체들에 대해서 정보를 제공한다.

이런 식으로 쓰고 싶은 이벤트를 쓴 다음 마우스를 올려보면 해당 이벤트에 대한 타입이 뜬다. 그러므로~ 그냥 이걸 복붙해서 타입으로 쓰면 된다.

const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => { ... }

event에 대한 정의는 이미 FormEventHandler가 정의해주었으니 따로 정의해주지 않아도 type을 가진다.

그러므로~ 이벤트를 지정해줄 때마다 일일이 찾아보진 않아도 되고, VSCode가 제공해주는 정보로 편하게 타입을 지정해주자.

여담

React + Typescript를 배우려면 꽤 러닝커브가 높겠다라고 생각한 게, 이런 타입들을 string, number처럼 하나하나 외워야 한다고 생각이 들어서였다. 그런데 이런 걸 보니 넘 지레 겁 먹은 느낌~ 해보기나 할 걸^~^

마무리로 적절한 음악을 추천드립니다... Typescript 찍먹 끄읕!


profile
FE개발자 가보자고🥳

0개의 댓글