[Type Script] Todo 앱 연습: 리액트에 타입스크립트를 더해 To Do App 만들기

summereuna🐥·2023년 8월 18일
0

TypeScript

목록 보기
2/13

CRA로 리액트 프로젝트 생성하며 Type Script 추가하기


npx create-react-app 앱이름 --template typescript

  • .jsx 대신 JSX 문법을 사용하는 .tsx를 사용
  • 컴파일은 빌드시 자동으로 이루어짐
  • 모든 라이브러리에 언어 변환이 필요한 것은 아니다. 어떤 라이브러리에는 타입 표기 기능이 내장되어 있어서 JS 만 사용해도 된다.
  • 하지만 몇몇 라이브러리는 타입 표기 기능이 없기 때문에 타입 표기를 위한 의존성이 추가된다.
    @types 패키지는 바닐라 JS와 TS 프로젝트 사이에서 번역기 역할을 한다.
    • TS 프로젝트에서 reactreact-dom 라이브러리를 사용하고, TS 및 개발 툴이 제공하는 기능과 자동 완성 같은 기능을 사용하기 위해서 언어 변환이 필요하다.
    • @types/react@types/react-dom은 타입 표기 기능을 자바스크립트 기반의 라이브 러리에 추가해 주는 거다.

타입스크립트를 사용하여 리액트 코드를 보완할 수 있다.
타입스크립트는 타입 추론을 사용할 때 사용한다. 아직은 사용할게 없음 ..^ㅇ^

타입스크립트의 핵심 기능을 익히기 위해 To Do App을 만들어 보자.

✅ Todos 컴포넌트 만들기


function Todos(props) {
  return <ul></ul>;
}
export default Todos;
  • 현재 props의 tyep을 명시하지 않았기 때문에 any type이라고 뜬다.
    그렇기 때문에 타입스크립트로부터 어떤 지원도 못 받는다.

  • 명시적으로 any타입을 지정하면 이 경고는 사라진다.
    따라서 명시적으로 타입을 설정하자!

📝 타입스크립트가 어디까지 경고 메시지를 보낼 것인지는 tsconfig.json에서 설정할 수 있다.

객체 타입 명시

리액트 18 이전 + TS


props: { items: string[] }

function Todos(props: { items: string[] }) {
  return <ul>{props.itmes}</ul>;
}
export default Todos;
  • props의 타입은 문자열 배열을 값으로 가진 items 키를 가진 객체이다.
  • 상위 컴포넌트에서 itmes={문자열배열 데이터} 를 itmes 프롭으로 보낼 거니까, 문자열 배열을 값으로 가지는 값을 items프롭으로 받는다.
    따라서 Todos 컴포넌트에서는 props.items을 매핑하여 배열의 각 요소를 꺼내 쓸 수 있다.

그런데 props은 보통, children을 가진다.

props: { items: string[], children: any }
  • 이 경우 위처럼 객체타입을 명시해서 사용하는 것 보다는 제너릭 타입을 사용하는 것이 좋다. 함수형 컴포넌트를 제너릭 함수로 변환하여 이용하는 것이다.

React.FC 사용하여 FuctionComponent 타입 명시하기

  • 이는 우리가 만든 함수형 컴포넌트에서 몇 가지 설정을 추가하여, 리액트 함수형 컴포넌트로 동작하도록 만들어 children과 같은 기본 props를 사용할 수 있도록 하기 위함이다.
import React from "react";

const Todos: React.FC<{ itmes: string[] }> = (props) => {
  return (
    //...
    )
};

export default Todos;
  • React.FC
    이 타입은 @types/react패키지에 정의된 타입으로, Todos 컴포넌트의 함수가 함수형 컴포넌트(FunctionComponent, FC) 타입, 즉 함수형 컴포넌트로 동작한다는 것을 명확히 하는 것이다.
  • FC가 이미 제네릭 타입이기 때문에, 홑화살괄호<>를 추가한 후 T 같은 식별자를 넣어 새로운 제네릭 타입을 만들지 않고 내부적으로 사용되는 FC의 제네릭 타입에 구체적인 값을 넣어 사용할 수 있다.
    • 그렇게 하는 이유는 타입스크립트에게 이 함수를 내부적으로 어떻게 처리해야 하는지 알려주기 위함이다.
    • 직접 추가한 props을 받아서 모든 함수형 컴포넌트가 가지고 있는 children 프로퍼티 같은 기본적인 props들과 합칠 수 있다.
  • 집어 넣을 값은 직접 만든 props 객체로, 함수형 컴포넌트에 맞게 props를 정의한 객체이다. 이것이 제네릭인 이유는 함수형 컴포넌트 마다 props에 대한 정의가 다르기 때문이다.

리액트 18 이후 + TS


그런데 저렇게 해보는 중, 원래는 props에 마우스 커서를 가져다 대면 items 뿐만 아니라 children도 표시가 되어야 하는데 난 items만 뜨고 children은 계속 뜨지 않았다.
왜 그런건지 구글링 해보니 리액트 18 업데이트 후 타입스크립트에서 FC children에 대한 부분이 바꼈다고 한다 ^^!ㅠ 어떻게 이렇게 딱!바뀔 수가...

  • React.FC 타입에 암묵적인 children 선언을 제거하는 업데이트가 있었다고 한다.
    암묵적으로 선언됐던 children을 명시적으로 컴포너느에 맞게 선언하자는 취지라고 한다.

참고한 블로그

React.FC 타입으로 children 선언하는 예제


  • 자식 컴포넌트가 꼭 필요한 경우 아래처럼 타입을 선언
type Props = {
  children: React.ReactNode;
};
  • 자식 컴포넌트가 꼭 필요한 경우가 아니라면 아래 처럼 옵셔널로 선언
type Props = {
  children?: React.ReactNode;
};
  • 표준적인 방법은 PropsWithChildren 사용
type Props = {
  title?: string;
};

const Component: React.FC<React.PropsWithChildren<Props>> = ({children}) => {
  return (
    <div>{children}</div>

Todos 컴포넌트에 업데이트 된 내용을 적용해 보자.

import React from "react";

type Props = {
  items: string[];
  children?: React.ReactNode; //children 옵셔널하게 넣기..
};

const Todos: React.FC<Props> = (props) => {
  return (
    <ul>
      {props.items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
};
export default Todos;

App.tsx에서 Todos 컴포넌트로 임의의 데이터를 보내보자.

import "./App.css";
import Todos from "./components/Todos";

const todos = ["test 1", "test 2"];

function App() {
  return (
    <div>
      <Todos items={todos} />
    </div>
  );
}

export default App;

잘되는구만!

✅ 데이터 모델 만들기


프로젝트에서 사용할 다양한 데이터 모델을 따로 models 폴더를 파서 만들어 보자.
컴포넌트 생성하지 않을 거니까 .ts 파일로 생성하자.

오호 기시감이 드는구만.. 이거 nodejs에서 몽구스 스키마 정하는거랑 비슷한거 아니여!!! 🤓

1. Todo 형태 정의 하는 파일 만들기


타입 생성자: type, interface, class

타입을 정의할 때는 type, interface로 타입을 생성할 수 있다. 혹은 class로 객체를 생성해도 된다.

📝 각 생성자에 대한 차이점
📝 Interface와 Class의 차이점

class는 객체 팩토리로 사용되기 때문에, 객체의 모양과 동작에 대한 청사진을 정의하여 다음 클래스 속성을 초기화하고 메서드를 정의한다.

  • 클래스의 인스턴스를 만들 때 실행 가능한 함수와 정의된 프로퍼티를 가진 객체를 얻을 수 있다.
  • 또는, 별도로 인스턴스를 만들지 않고도 static 함수를 사용하여 인스턴스를 만들 수 있다. (인터페이스에서는 타입 체크만 가능하고 이런게 불가능함)

하나 만들어 놓고 찍어 내고 싶을땐 class 를 사용하면 된다고 한다.
이건 나중에 좀 더 공부를 해보자구..

class로 타입 생성하여 Todo 형태 정의하는 .ts 파일 만들기

📍 /src/models/todo.ts

//Todo의 형태 정의하는 파일
//바닐라 자바스크립트에서 class 사용하던거 처럼 하면 된다.
class TodoClass {
  //형태 정의시에는 타입스크립트에서 class를 사용할 때는, 클래스에 추가할 프로퍼티가 있는 경우 바닐라에서 하듯이 생성자를 통해 추가할 필요가 없다.
  //대신 class에 바로 추가할 수 있다.
  id: string;
  text: string;
}

export default TodoClass;
  • 그런데 이렇게 생성하면 id, text에 빨간 밑줄이 그이는데 마우스 커서를 가져다 대면, "text 프로퍼티를 초기화하는 부분이 없고, 생성자에서 값이 할당되지 않았습니다."라는 메시지가 뜬다.
  • 이 경고 메시지의 뜻은, 여기에 정의된 이 클래스가 인스턴스화 되어야 한다는 뜻이다. 그게 클래스를 사용하는 주된 목적이다.

📍 /src/models/todo.ts

//Todo의 형태 정의하는 파일

class TodoClass {
  //타입 지정 (TS만 있는 부분)
  id: string;
  text: string;

  //생성자 만들어 값 할당 (JS에도 있는 부분)
  //인수로 todoText 보내기, id는 생성자 안에서 동적으로 만들기
  constructor(todoText: string) {
    this.text = todoText;
    this.id = new Date().toISOString(); //임의 id 만들기 위해 만들어진 날짜 id로 넣어주자
  }
}

export default TodoClass;

이제 Todo 클래스를 사용할 준비를 끝냈기 때문에 App.tsx에서 사용할 수 있다.

2. TodoClass 클래스 사용하기


📍 /src/App.tsx

  • new TodoClass()로 투두를 생성하고 인자(todoText)를 보낸다.
  • 이제 todos 배열은 문자열 배열이 아닌, TodoClass 객체를 가지는 배열이 된다.
import "./App.css";
import Todos from "./components/Todos";
import TodoClass from "./models/todo";

//이제 todos 배열은 문자열 배열이 아닌, TodoClass 객체 배열이다.
const todos = [
  new TodoClass("test1"),  //new Todo(todoText)로 투두를 생성하고 인자를 보낸다.
  new TodoClass("test2")
];

function App() {
  return (
    <div>
      <Todos items={todos} />
    </div>
  );
}

export default App;

📍 /src/component/Todos.tsx

  • todos가 TodoClass 객체를 가지는 배열이므로, todos를 받는 items 프로퍼티도 TodoClass 객체를 가지는 배열이 된다.
    기존에 설정해둔 타입과 달라져 오류가 뜨기 때문에 타입을 수정해야 한다. {}[]이렇게 수정해도 되겠지만 좀 더 분명한 방법이 있다.

정의된 클래스는 타입으로도 사용할 수 있다.

  • 🔥 정의된 클래스는 새로운 객체를 생성하는 생성자 역할을 할 뿐만 아니라 타입 역할도 한다!
  • items: TodoClass[];
    items는 TodoClass 객체로 채워진 배열로, 배열의 객체는 문자열 타입을 가진 id 프로퍼티와 text 프로퍼티를 가진다.
  • 따라서 jsx 부분에 items 배열을 매핑할 때, 각 객체의 프로퍼티(item.id, item.text)를 사용할 수 있게 된다.
import React from "react";
import TodoClass from "../models/todo";

type Props = {
  items: TodoClass[]; //items은 TodoClass 객체로 채워진 배열
  // 배열의 객체는 문자열 타입을 가진 id프로퍼티와 text프로퍼티를 가진다.
  children?: React.ReactNode; //옵셔널
};

const Todos: React.FC<Props> = (props) => {
  return (
    <ul>
      {props.items.map((item) => (
        <li key={item.id}>{item.text}</li>
        //따라서 items의 각요소의 id, text 프로퍼티를 사용할 수 있다.
      ))}
    </ul>
  );
};
export default Todos;

📝 타입 스크립트의 장점


  • 클래스 덕분에 어떤 형태의 데이터와 컴포넌트가 필요한지 명확해졌다.
  • 이렇게 하면 구조가 명확해 지기 때문에 컴포넌트나 데이터를 잘못 사용하는 일도 훨씬 줄어든다.
  • 또한 오류를 개발 과정에서 미리 수정할 수 있다. 다른 타입의 todos 데이터를 넘기면 코드 편집창에 바로 경고 메시지가 나타나기 때문이다.

🔥 연습: Todo 아이템 컴포넌트를 하나 더 만들어 타입을 지정해 보자.


1. Todo 컴포넌트 생성하여 타입 명시하기

📍 /src/component/Todo.tsx

  • Todo: React.FC
    함수형 컴포넌트를 만들 때 React.FC 타입 표기를 추가하여 이 컴포넌트가 함수형 리액트 컴포넌트임을 명시한다.
  • <{text: string}>
    제네릭을 사용하여 props가 가진 기존 프로퍼티에 text 프로퍼티를 추가한다.
const Todo: React.FC<{ text: string }> = (props) => {
  return <li>{props.text}</li>;
};

export default Todo;

2. Todos 컴포넌트에서 Todo 컴포넌트로 props 보내기

📍 /src/component/Todos.tsx

  • Todos 컴포넌트의 jsx에서 Todo 컴포넌트를 가져와 items 배열에 매핑한다.
  • key 값은 매핑하면서 바로 할당해줘야 한다.
//...

const Todos: React.FC<Props> = (props) => {
  return (
    <ul>
      {props.items.map((item) => (
        <Todo key={item.id} text={item.text} />
      ))}
    </ul>
  );
};

//...
profile
Always have hope🍀 & constant passion🔥

0개의 댓글