타입스크립트로 리액트 함수형 컴포넌트 작성하는 방법
시작 전에
함수형 컴포넌트에 대해 알아보기 전에 이 글을 보시는 분들은 사전에 리액트 프로젝트가 설치되었다는 가정하에 진행합니다. 만약 리액트 프로젝트가 설치되지 않았다면 리액트 프로젝트 설치 후 진행해 주세요!
# 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;
}
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 를 빠뜨리게 된다면 다음과 같이 에디터에 오류가 나타납니다.
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 타입을 디폴트로 사용할 수 있습니다.
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;
...
}
개발에 정답은 없습니다. 프로젝트 초기에 팀원들끼리 명확하게 세운 코딩 컨벤션에 맞춰서 사용하시면 됩니다.