타입스크립트에서 Hooks를 이용한 React 함수형 컴포넌트

이성진·2020년 6월 7일
0

번역

목록 보기
1/1
post-thumbnail

안녕하세요. 오랜만에 velog에 글을 쓰려고 왔습니다.
기본적으로 React에서 함수형 컴포넌트를 써본사람이 Typescript를 적용시키려 할 때 방황합니다.
저의 경우 하나의 라이브러리를 사용하는건 어렵지 않지만 두 개, 세 개가 되면 둘 사이를 어떻게 처리해야할 지 모르겠거든요...

그래서 이것에 관한 글을 읽다가 좋은 글이 있길래 번역해서 올립니다.

원본 출처: Using React Functional Components with Hooks in TypeScript

사실 영어 본문 읽을 줄 아시면 원본 보시는 것을 추천해요.

타입스크립트를 이용한 리엑트 클래스 컴포넌트는 React.component를 상속함으로서 자연스럽게 다가왔습니다. 그리고 타입을 지정함으로서 즉각적인 이득을 봤습니다.
그러나 리액트 함수형 컴포넌트에선 가끔 혼란을 일으킨다.

리액트 함수형 컴포넌트는 자바스크립트의 간단한 기능(functions)이다. 그러나, 이것이 타입스크립트에서 어떠한 제약도 없다는 것을 의미하지는 않는다.

명확하게 그리고 중요하게, 함수형 컴포넌트는 우리가 필요한 타입의 props로 오브젝트를 취할 수 있다.

타입스크립트로 함수형 컴포넌트를 만들 때, 우리는 React.FC 타입을 처음 사용한다. 이것은 FunctionComponent 인터페이스에 기반을 두고 있다.

type FC<P = {}> = FunctionComponent<P>;

interface FunctionComponent<P = {}> {
  (props: PropsWithChildren<P>, context?: any): ReactElement | null;
  propTypes?: WeakValidationMap<P>;
  contextTypes?: ValidationMap<any>;
  defaultProps?: Partial<P>;
  displayName?: string;
}

React.FC는 우리 함수의 명시가 확실한지 확인합니다. 그리고 올바른 JSX 값을 반환합니다. 뿐만 아니라, 타입스크립트의 제네릭(generics)을 통하여 인터페이스 또는 타입을 인라인 props로 넘겨줄 수 있습니다. 이렇게 넘겨진 타입은 props, defaultProps(neatly using the utility type Partial) 및 propTypes(예전에 사용된 타입스크립트)에 사용됩니다. 그렇게 모든것이 타입이 됩니다.

import React from 'react';

export const Message: React.FC<{ text: string }> = ({ text }) => {
  return <span>{text}</span>;
}

위의 코드는 우리가 사용하는 함수형 컴포넌트를 의미하고, 구체적인 props를 설정하고 넘겨주어야 합니다.(여기서는 문자열 타입의 text). 주의해야 할 점, 구조 분해 할당을 사용한다면, 간결하게 접근할 수 있어야 합니다. 만약 prop 타입을 주지 않거나, prop을 추가하는 것을 까먹는 다면, 컴파일 시 에러가 날 것입니다.

// Type 'number' is not assignable to type 'string'
// 타입 'number'는 'string' 타입에 할당할 수 없습니다.
<Message text={5000} />; // Error!

// Property 'text' is missing in type '{}' but required in type '{ text: string; }'
// 속성 'text'가 '{}'에 없습니다. '{ text: string; }' 타입이 필수되어야 합니다.
<Message />; // Error!

// Type '{ msg: string; }' is not assignable to type 'string'
// 타입 '{ msg: string; }'을 'string' 타입에 할당할 수 없습니다.
<Message text={{ meg: "Hello World" }} />

심지어 메시지의 속성으로 기본값(default value)을 준다 하더라도, 에러를 넘겨줘야 합니다. 그렇기 때문에 text 속성을 선택적으로 넘겨야 합니다. 아래와 같이:(This is because our text property type needs to be an optional, as such:)

import React from "react";

export const Message: React.FC<{ text?: string }> = ({ text = "hey" }) => {
  return <span>{text}</span>;
};

export const someComp: React.FC = () => {
  return <Message />;
};

선택적이지 않은 필드에서도 기본값을 설정할 수 있다는 사실은 매우 이상하며 컴파일 시 아무런 영향을 미치지 않습니다.
(의역이 섞여서 원본 놓겠습니다. The fact that setting default values is even possible for non-optional fields is quite weird to me and does not have any effect at compile time.)

훅(Hooks)

Hooks는 함수형 컴포넌트에게 힘을 실어줍니다. 물론 우리는 여전히 강력한 타이핑의 혜택을 받고 있습니다. 가장 위험한 것중 하나는 useState hook의 기본값을 설정하는 데에 있습니다.

export const SomeComponent: React.FC = () => {
  // Property 'text' is missing in type 'undefined' but required in type 'SomeType'
  const apiObject: SomeType | undefined = useState(undefined);
  const isLoading = useState(false);
  
  useEffect(() => {
    // fetches apiObject from some API
  }, []);
  
  return <>{isLoading ? <span>Loading</span> : <span>{apiObject.text}</span>}</>;
  // Error on 'apiObject' 
  // apiObject는 undefined가 할당될 수 있으므로 
  // .text를 참조하면 오류 -> 타입 가드를 이용해서 오류 방지  
};

위의 코드는 컴파일 되지 않을 것이며 타입스크립트도 기본값을 제공하는 것을 허락하지 않을 것입니다. 위의 코드가 에러가 나는 이유는 타입스크립트가 apiObject 를 유니온 타입 someType | undefined로 정의했음에도 불구하고 기본값에 의하여 undefined로 추론하기 때문입니다.

우리는 훅(hooks)을 사용할 때 제네릭(generics)을 통하여 이 문제를 해결할 수 있습니다. 왜냐하면 옵셔널과 같은 유니온 타입의 기본값에 대해서는 타입 추론이 여러분을 방해하기(bite) 때문입니다. 만약 undefined를 객체의 기본값으로 주길 원한다면, 아래와 같이 타입 정보를 넘겨줌으로서 가능합니다.

export const SomeComponent: React.FC = () => {
  const [apiObject, setApiObject] useState<SomeType | undefined>(undefined);
  const isLoading = useState(false);
  
  useEffect(() => {
    setApiObject({ text: "Hello world" });
  }, []);
  
  return <>{isLoading ? <span>Loading</span> : apiObject ? <span>{apiObject.text}</span> : <span>Error!</span>}</>;
};

이것은 또한 함수형 컴포넌트 안에서 객체를 접근하는 것을 확인하는 것을 강제합니다.

useReducer 훅을 활용하는 것과 비슷합니다. 그저 액션(action)과 스테이트 타입을 제공하면 됩니다. 만약 그렇게 하지 않으면, 초기 스테이트 타입을 사용했을 때 타입 추론에서 동일한 문제가 발생할 것입니다. 리듀서 함수에게 제공해야 하는 것

  • 첫 번째 매개변수로 행동 타입(action type)을 받아야 합니다. (Actionn)
  • 두 번째 매개변수로 스테이트의 타입을 받아야 합니다. (SomeType)
  • 스테이트 타입과 동등한 객체를 반환합니다. (SomeType)
  • useReducer는 스테이트에 초기값을 제공해야 합니다. (SomeType)
interface SomeType {
  text: string;
}

interface Action {
  type: "string";
}

const reducer = (state: SomeType, action: Action) => {
  return state;
};

// reducer

export const SomeComponent: React.FC = () => {
  const [state, dispatch] = useReducer<typeof reducer>(reducer, { text: "" });
  
  return <></>;
};

결론

함수형 컴포넌트는 그저 함수일 뿐입니다. 그러나, 함수형 컴포넌트가 효과적이지 않게 타입이 적용되어야 한다는 것을 의미하지는 않습니다. 읽어주셔서 감사합니다.


오랜만에 한 번더 영어 원문? 을 번역했습니다...

항상 번역할 때 모르는 단어나 문장이 두 개 이상은 있네요.

그렇지만 이렇게 공부하는게 제 기준에서 너무 좋고 잘 되는거 같아서 기분이 좋네요.

제 글 읽어주셔서 감사합니다. (__)

profile
개발보다 회사 매출에 영향력을 주는 개발자

0개의 댓글