[TS] React + Typescript

RINM·2024년 6월 20일

Study

목록 보기
6/7

FE + Typescript

Typescript로 백엔드 개발은 꽤 해봤지만 프론트에 적용하는 것은 처음이다. 타입스크립트가 어떤 언어고 어떻게 사용하는지는 알지만 React에 어떻게 적용해야하는지 감이 안 와서 공부를 해보려한다.
짧고 간결한 인프런 강의를 하나 발견했다. 2시간 안에 Typescript로 FE를 만들 수 있게 도와준다.

React와 Vue로 배우는 TypeScript 필수 개념 (feat. ChatGPT)

FE의 측면에서 타입스크립트를 사용하는 이유는 주로 다음과 같다.
1. 사용자 경험: 실행 시점의 에러를 어느정도 미리 잡아줄 수 있다. build 과정에서 type을 어겼을 때 나타날 수 있는 에러를 잡아준다.

예를 들어 아래의 간단한 sum 함수가 있다고 하자. 기존의 javascript에서는 아래와 같이 함수를 정의하고 사용할 것이다.

function sum(a, b){
	return a+b;
}

sum(10,20); // return 30
sum(10,"20"); // return "1020"

정말 간단한 함수이지만, 함수에 인자로 숫자가 아닌 다른 타입의 값을 넘기게 되면 생각처럼 동작하지 않거나 오류가 나게 된다. 예를 들어 숫자 20대신 문자열 '20'을 넘겨주면 기대한 30이라는 숫자값이 아니라 문자열 "1020"이 반환된다.

typescript를 사용하면 변수의 타입을 충분히 직관적으로 알 수 있다.

function sum(a: number, b: number){
	return a+b;
}

sum(10,20); //return 30
sum(10,"20"); //error

인자값에 문자열을 잘못 코드 레벨에서 오류가 표시되기 때문에 더 직관적으로 이해할 수 있다. 이를 통해 사용자에게 노출되는 오류를 줄일 수 있다.

  1. 개발자 경험: 개발하는 과정에서 코드를 편하게 자동 완성하거나 코드 역할에 대해서 정보를 제공해주는 역할을 한다.

앞서 살펴본 javascript로 작성된 sum 함수는 개발자 관점에서도 불편하다. 인자에 어떤 값이 들어가야하는지 sum이라는 함수를 사용하는 입장에서는 알 수가 없기 때문이다. 물론 jsdocs로 설명을 달아놓을 수는 있겠지만 한 눈에 알 수 있는 것은 아니다.

코드를 만드는 과정에서도 타입은 매우 유용하다. VSC에서는 변수의 타입을 알고 있는 경우 그 객체에 포함된 매서드와 변수 목록을 자동으로 제공한다. 자동 완성을 통해 훨씬 빠르게 코드를 작성할 수 있는 것이다.

React + Typescript

Functional component

컴포넌트는 아래와 같이 정의한다. jsx 파일 대신 tsx 라는 확장자를 사용한다.

function App(): JSX.Element {
	return(
    	<div>App</div>
    );
}

JSX.Element라는 타입을 명시적으로 지정해주지 않아도 자동으로 추론해주기는 한다.

Function

함수 정의는 BE에서 하던 것과 같다. 파라미터와 반환타입을 ":"로 anotaion해줄 수 있다.

function sum(a: number, b: number): number {
	return a+b;
}

State

상태는 초기값에 따라서 자동으로 타입이 지정된다. 예를 들어 초기값으로 0을 주면 자연스럽게 number 타입으로 지정된다. (타입 추론) 물론 generic하게도 줄 수 있다.

const [counter, setCounter] = useState(0) // number
const [productList, setProductList] useState<Product []>() // generic

Event Handler

event의 타입은 react에서 정의한 각 Event를 가져와서 지정해줄 수 있다.
예를 들어 버튼 클릭의 경우 아래와 같이 MouseEvent를 가져와서 generic으로 HTMLButtonElement를 넣어준다.

const showAlert = (e : MouseEvent<HTMLButtonElement>) => {
    console.log(e);
};

return (
	<button onClick={showAlert}>show</button>
)

input 태그의 onChange는 React.ChangeEvent< HTMLInputElement >로 아래처럼 처리한다.

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

const updateProductPrice = (e : React.ChangeEvent<HTMLInputElement>) => {
  setNewProductPrice(parseFloat(e.target.value));
}
  
<input type='text' placeholder='Product Name' onChange={updateProductName} />
<input type='number' placeholder='Product Price' onChange={updateProductPrice}/>

Generic으로 태그의 타입을 넣어주지 않으면 Element 타입으로 들어간다.

Generic

타입을 함수의 파라미터로 사용하려면 Generic으로 함수를 선언한다. 무슨 말이냐면, 인자로 들어올 값의 타입을 호출할 때 지정하는 것이다. 예를 들어 아래처럼 선언한 getText라는 함수가 있다. 이 함수는 인자로 받은 값을 그대로 리턴한다. 사이에 로그를 찍어서 text의 타입을 확인해보았다.

function getText<T>(text: T){
  console.log(typeof text);
  return text;
}

getText<string>('Hello'); // string
getText<number>(123);

string 타입으로 지정하면 string이 콘솔에 찍히고, number로 지정하면 number가 콘솔에 찍힌다.

만약 generic으로 넘겨준 타입과 인자의 타입이 맞지 않는다면 다음과 같이 코드 단에서 오류를 잡아준다.

객체의 모양이 똑같거나, 함수에서 다루는 객체의 멤버가 같아서 여러 타입을 같은 함수로 처리하고 싶을 때 쓸 수 있다.

함수에만 generic을 쓸 수 있는 것은 아니다. object를 선언할 때도 Generic으로 만들 수 있다.
예를 들어 아래와 같이 멤버 변수의 이름은 다르지만 타입이 다른 두 interface Person과 Man이 있다고 하자.

interface Person {
  name: string
  age: number
}

interface Man {
  name: string
  age: string
}

이 두 inteface로 객체를 생성하려는데 어떨 때는 age에 number 타입이, 어떨 때는 string 타입이 들어올 수 있다. 그럼 이 객체를 선언할 때마다 들어오는 값에 맞추어서 Person과 Man으로 타입을 선언해야하는 번거로움이 있다.
이 부분을 Generic으로 선언하면 편리해진다. 아래의 interface Human은 Generic으로 age의 타입을 받아서 사용한다.

interface Human<T> {
  name: string
  age: T
}

const john: Human<string> = {
  name: 'John',
  age: "30"
}

const jane: Human<number> = {
  name: 'John',
  age: 30
}

그럼 string으로 선언한 john이든, number로 선언한 jane이든 모두 Human으로 선언할 수 있다.
비슷한 타입들이 많고 일정 부분만 변경될 때 사용하기 매우 편리하다. react 뿐만아니라 다양한 외부 라이브러리가 generic으로 함수를 선언해두었기 때문에 쓸일이 많을 것이다.

Type compatibility

타입은 호환된다. 어떻게? any 타입이 대표적인 예이다. any로 선언된 변수는 어떤 타입의 변수가 오든 호환이 된다.

let a: number = 10;
let b: string = "yes";
let c: any = false

b = a // error
c = a // compatible

위의 예제에서 string으로 선언된 b에는 숫자 값인 a가 대입될 수 없다. 하지만 any 타입으로 선언된 c의 경우 number로 선언된 a든, string으로 선언된 b든 모두 담을 수 있다.

React Props

부모 컴포넌트에서 전달 받은 props도 타입을 지정해줘야한다. 타입 정의는 interface나 type으로 한다. 대부분 props는 객체이기 때문에 전달받을 항목들과 그 각각의 타입에 맞춰서 정의해주면 된다.

interface

  • 객체 모양의 타입을 정의할 때 유용. 주로 API 응답, props 정의에 사용
  • 상속, 확장 가능
  • 사용할 데이터의 형상을 미리 정의해두는 것
interface Product{
  id: number
  name: string
  price: number
}

const productList : Product [] = [
        { id: 1, name: 'Product 1', price: 100 },
        { id: 2, name: 'Product 2', price: 200 },
        { id: 3, name: 'Product 3', price: 300 },
    ]

type alias

  • 타입에 의미를 부여하는 문법
  • 타입을 변수 레벨로 정의하는 것
  • 재선언 불가
  • union (|), intersaction(&) 으로 정의 가능
type Product = {
  id: number
  name: string
  price: number
}

const productList : Product [] = [
        { id: 1, name: 'Product 1', price: 100 },
        { id: 2, name: 'Product 2', price: 200 },
        { id: 3, name: 'Product 3', price: 300 },
    ]

API Type Definition

REST API의 타입을 정의하는 방법을 알아보자.
JSONPlaceholder의 fake API를 받는 함수 fetchTodos를 만들어준다. JSONPlaceholder의 todos API는 이렇게 생겼다.

타입을 생각하지 않고 데이터를 받아오는 부분만 짠다면 fetchTodos는 아래와 같은 모습을 가질 것이다.

const fetchTodos = async () => {         
  const response = await fetch(apiURL);
  const data = await response.json();
  return data
}

우선 todo 객체의 형태를 interface로 만들어주자.

interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

fetchTodos는 이 Todo 타입을 받는 async 함수일테니, Promise< Todo >를 반환값의 타입으로 주면 된다.

const fetchTodos = async () : Promise<Todo> => {         
  const response = await fetch(apiURL);
  const data = await response.json();
  return data
}

fetchTodos().then((todo)=> console.log(todo)

이렇게 완성한 fetchTodos를 실행해보면 로그가 잘 찍힌 것을 볼 수 있다.

0개의 댓글