개발/React

타입스크립트로 리액트 함수형 컴포넌트 작성하는 방법

남양주개발자 2021. 11. 8. 08:51
728x90
반응형

리액트 함수형 컴포넌트 타입스크립트로 작성하는 방법 How to Use TypeScript with React Functional Components

시작 전에

함수형 컴포넌트에 대해 알아보기 전에 이 글을 보시는 분들은 사전에 리액트 프로젝트가 설치되었다는 가정하에 진행합니다. 만약 리액트 프로젝트가 설치되지 않았다면 리액트 프로젝트 설치 후 진행해 주세요!

# React CRA
npx create-react-app my-app --template typescript
# or
yarn create react-app my-app --template typescript
# Next.js
npx create-next-app@latest --typescript
# or
yarn create next-app --typescript

함수형 컴포넌트 만들기

리액트에서 타입스크립트를 사용하지 않고 함수형 컴포넌트를 만드는 방법은 아래와 같습니다. 화살표함수나 일반 함수 정의로 만들 수 있습니다.

import React from "react";

const Child = () => {
  return <div>hello</div>;
};

export default Child;

화살표 함수로 함수형 컴포넌트를 생성할 경우 아래처럼 리턴(return) 구문을 생략하고 구현할 수 있다는 이점이 있습니다. (만들기 편한 방법으로 컴포넌트를 생성하면 됩니다.)

import React from 'react';

const Child = ({ text }) => <div>{text}</div>

export default Child;

함수 정의 구문으로 컴포넌트를 생성하는 방식은 아래와 같습니다. 일반적으로 리액트 함수형 컴포넌트를 작성할 때 아래 패턴으로 구현하곤 합니다.

import React from "react";

function Child() {
  return <div>hello</div>;
}

export default Child;

타입스크립트로 작성할 때 설명드리겠지만, 아래 방식으로 함수를 정의하면 타입스크립트의 React.FC 타입을 활용하는데 어려움이 있어서 화살표 함수를 통해 구현하곤 합니다.

import React from "react";

export default function Child() {
  return <div>hello</div>;
}

타입스크립트로 작성하기

타입스크립트를 사용한다면 리액트 컴포넌트 파일은 *.tsx 확장자를 사용합니다. Child.tsx 컴포넌트를 만들어 봅시다. React는 typescript로 작성되지 않았기 때문에 @types/react 패키지를 사용합니다. 이 패키지에서 FC라고 불리는 함수형 컴포넌트 타입을 제공합니다.

기본 구조

import React, { FC } from "react";

// Child: React.FC or Child: FC
const Child: FC = () => {
  return <div>hello</div>;
};

export default Child;

리액트 React.FC 인터페이스 전체 구조는 아래와 같습니다. React.FC를 활용하면 아래 인터페이스를 모두 기본적으로 갖고 활용할 수 있습니다.

// @types/react/index.d.ts 리액트 타입 정의 파일
type FC<P = {}> = FunctionComponent<P>

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

react index.d.ts 타입 확인하는 예시

Props 인터페이스 추가

컴포넌트에서 사용하는 props에 대한 인터페이스를 정의하는 방법은 아래와 같습니다. interface Props에 사용하는 props 값들을 정의합니다. React.FC를 사용한다면 Generics 로 넣어서 사용합니다. 아래는 Props 인터페이스에 text를 정의한 예시입니다.

import React from "react";

// 인터페이스 추가!
interface Props {
  text: string;
}

const Child: React.FC<Props> = ({ text }) => {
  return <div>{text}</div>;
};

export default Child;

인터페이스를 추가하고 Child 컴포넌트를 추가하면 필수적으로 사용되는 text 값이 없기 때문에 타입스크립트에서 에러를 발생하는 것을 확인할 수 있습니다. TypeScript 를 사용하신다면 컴포넌트를 렌더링 할 때 필요한 props 를 빠뜨리게 된다면 다음과 같이 에디터에 오류가 나타납니다.

Property 'text' is missing in type '{}' but required in type 'Props'

Child 컴포넌트에 text를 넣어주면 정상적으로 에러없이 동작하는 것을 확인할 수 있습니다!

Child 컴포넌트에 text 값을 주입시키는 모습

매끄럽게 동작하지 않는 defaultProps

타입스크립트와 리액트 defaultProps를 조합해서 사용하면 기본 속성 값들이 제대로 확인되지 않는 버그가 존재합니다.

// Child Component
import React from 'react';

interface Props {
  text: string
}
const Child:React.FC<Props> = ({ text }) => {
  return (
    <div>
      {text}
    </div>
  )
}

Child.defaultProps = {
  text: 'hello'
}

export default Child;

text 를 defaultProps 로 넣었음에도 불구하고, text 값이 존재하지 않는다는 에러 문구가 나타나는 것을 확인할 수 있습니다.

그렇기 때문에 defaultProps를 활용하기엔 조금 문제가 있고, 타입스크립트 Optional과 자바스크립트 기본값 함수 매개변수를 활용해서 처리합니다.

import React from "react";

// text 프로퍼티를 Optional로 변경!
interface Props {
  text?: string;
}

// 자바스크립트 기본값 함수 매개변수 추가!
const Child: React.FC<Props> = ({ text = "hello" }) => {
  return <div>{text}</div>;
};

export default Child;

확인해보면 타입스크립트 에러없이 잘 동작하는 것을 확인할 수 있습니다! 

타입스크립트 에러없이 잘 동작하는 모습

반면, React.FC 를 생략하면?

// Child Component
import React from 'react';

interface Props {
  text: string
}

function Child({ text }: Props) {
  return (
    <div>
      {text}
    </div>
  )
}

Child.defaultProps = {
  text: 'hello'
}

export default Child;

타입스크립트와 defaultProps가 자연스럽게 잘 동작하는 모습입니다.

생략을 하면 오히려 잘 동작하는 것을 확인할 수 있습니다! 이러한 이슈때문에 React.FC 를 굳이 사용하지 않아도 된다는 이야기도 있습니다.

children

React.FC를 사용하면 props.children 타입을 디폴트로 사용할 수 있습니다.

props.children 타입이 기본으로 정의되는 모습

children props를 명시적으로 필요한 곳에 사용하고 싶은 분이라면 단점일 수 있고, 굳이 크게 신경 안쓰시는 분이라면 편하게 사용할 수 있는 장점이 될 수 있어보입니다. 

type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
type PropsWithChildren<P> = P & { children?: ReactNode | undefined };

interface FunctionComponent<P = {}> {
    (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
	...
}

개발에 정답은 없습니다. 프로젝트 초기에 팀원들끼리 명확하게 세운 코딩 컨벤션에 맞춰서 사용하시면 됩니다.

728x90
반응형
그리드형