저번에는 타입스크립트의 기초를 공부해보았습니다.
이번에는 리액트 컴포넌트를 타입스크립트로 작성하는 방법에 대하여 공부해보겠습니다.
우선, 타입스크립트를 사용하는 프로젝트를 생성해 봅시다.
$ npx create-react-app ts-react-tutorial --typescript
위와 같이 뒤에 --typescript가 있으면 타입스크립트 설정이 적용된 프로젝트가 생성되게 됩니다.
만약, 이미 만든 프로젝트에 타입스크립트를 적용하고 싶다면 이 링크를 확인해주세요.
이제 프로젝트를 열어보면 src 디렉터리 안에 App.tsx라는 파일이 있을 것입니다. 타입스크립트를 사용하는 컴포넌트는 이와 같이 *.tsx 확장자를 사용합니다. 해당 파일을 한번 열어봅시다. (제가 할 때는 처음부터 const App: React.FC = () => { ... } 이 아닌 function App() { ... } 형태로 선언되어 있었습니다.)
import React from 'react';
import logo from './logo.svg';
import './App.css';
const App: React.FC = () => {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
이 컴포넌트를 보면, const App: React.FC = () => { ... } 와 같이 화살표함수를 사용하여 컴포넌트가 선언되어 있습니다. 그런데, 꼭 화살표 함수를 사용하여 선언할 필요는 없습니다. 요새는 보통 function 키워드를 사용하여 함수형 컴포넌트를 선언하는 것이 추세입니다. 리액트 공식 메뉴얼에서도 function 키워드를 사용하고 있기도 하죠.
반면 프로젝트를 만들 때 자동생성 된 App.tsx의 경우 React.FC라는 타입을 사용하여 화살표 함수를 사용하면서 컴포넌트를 선언했는데요, 이렇게 타입하는 것이 좋을 수도 있고, 나쁠 수도 있습니다. 벨로퍼트 님은 이 방식이 나쁘다고 생각하십니다. 그 이유는 밑의 공부 내용에서 알아보겠습니다.
한번, 새로운 컴포넌트를 작성해 보면서 React.FC를 사용하는 것과 사용하지 않는 것의 차이를 알아보도록 하겠습니다.
Greetings라는 새로운 컴포넌트를 작성해 봅시다.
import React from 'react';
type GreetingsProps = {
name: string;
};
const Greetings: React.FC<GreetingsProps> = ({ name }) => (
<div>Hello, {name}</div>
);
export default Greetings;
컴포넌트의 props에 대한 타입을 선언 할 때에는 type을 써도 되고, interface를 써도 됩니다. 쓰는 사람 자유이기 때문에, 마음대로 해도 되지만 프로젝트에서 일관성만 유지하면 된다고 합니다. (A에서는 type 쓰고 B에서는 interface 쓰고 이러면 안된다. 이런거라고 하네요)
React.FC를 사용할 때는 props의 타입을 Generics로 넣어서 사용합니다. 이렇게 React.FC를 사용해서 얻을 수 있는 이점은 두 가지가 있습니다.
1번째는, props에 기본적으로 children이 들어가 있다는 것입니다.
중괄호 안에서 Ctrl + Space를 누르면 확인할 수 있습니다.
2번째는 컴포넌트의 defaultProps, propTypes, contextTypes를 설정할 때 자동완성이 될 수 있다는 것입니다.
한편으로는 단점도 존재한다고 합니다. children 이 옵셔널 형태로 들어가 있다 보니 어찌 보면 컴포넌트의 props 타입이 명백하지 않습니다. 예를 들어 어떤 컴포넌트는 children이 무조건 있어야 하는 경우도 있을 것이고, 어떤 컴포넌트는 children이 들어가면 안되는 경우도 있을 것입니다. 결국 그에 대한 처리를 하고 싶다면 Props 타입 안에 chilren을 명시해야 합니다.
예를 들면 다음과 같다고 합니다.
type GreetingsProps = {
name: string;
chilren: React.ReactNode;
};
결국 React.FC에 props가 기본적으로 들어있는건 어찌 보면 장점이 아닙니다. 차라리, React.FC를 사용하지 않고 GreetingsProps 타입을 통해 children이 있다 없다를 명백하게 명시하는 게 덜 헷갈립니다.
추가적으로, (2019.10 기준) React.FC를 사용하는 경우 defaultProps가 제대로 작동하지 않는다고 합니다. 이는 정말 치명적입니다. 다음과 같이 코드를 작성했다고 가정해 봅시다.
import React from 'react';
type GreetingsProps = {
name: string;
mark: string;
};
const Greetings: React.FC<GreetingsProps> = ({ name, mark }) => (
<div>Hello, {name} {mark}</div>
);
Greetings.defaultProps = {
mark: '!'
};
export default Greetings;
import React from 'react';
import Greetings from './Greetings';
const App: React.FC = () => {
return <Greetings name="Hello" />;
};
export default App;
mark를 defaultProps 로 넣었음에도 불구하고 mark 값이 없다면서 제대로 작동하지 않습니다. (공부한 날짜 2021.01.19 기준)
React.FC를 쓰면서 defaultProps를 사용하려면 결국 코드를 다음과 같이 작성하는 수 밖에 없습니다.
import React from 'react';
type GreetingsProps = {
name: string;
mark: string;
};
const Greetings: React.FC<GreetingsProps> = ({ name, mark = '!' }) => (
<div>
Hello, {name} {mark}
</div>
);
// 결국 무의미해진 defaultProps?
Greetings.defaultProps = {
mark: '!'
};
export default Greetings;
바로, 위와 같이 비구조화 할당을 하는 과정에서 기본값을 설정해 주면 됩니다. 이러면 밑에 있는 defaultProps는 참 무의미해집니다.
반면, 만약 React.FC를 생략한다면 어떻게 될까요?
import React from 'react';
type GreetingsProps = {
name: string;
mark: string;
};
const Greetings = ({ name, mark }: GreetingsProps) => (
<div>
Hello, {name} {mark}
</div>
);
Greetings.defaultProps = {
mark: '!'
};
export default Greetings;
React.Fc를 생략하면, 오히려 아주 잘 작동합니다! 이러한 이슈 때문에 React.FC를 사용하지 않는 것이 좋습니다. 이를 쓰고 안쓰고는 자유지만, Velopert님의 포스팅에서는 사용하지 않는 것이 좋다고 합니다.
취향에 따라 화살표 함수도 사용하지 않는다면 다음과 같은 형태가 됩니다.
import React from 'react';
type GreetingsProps = {
name: string;
mark: string;
};
function Greetings({ name, mark }: GreetingsProps) {
return (
<div>
Hello, {name} {mark}
</div>
);
}
Greetings.defaultProps = {
mark: '!'
};
export default Greetings;
화살표 함수를 쓸지, function 키워드를 사용해서 선언을 할 지는 쓰는 이 자유입니다. 다만, React.FC의 사용은 권장하지는 않는다고 합니다.
만약에 컴포넌트의 props 중에서 생략해도 되는 값이 있다면, ? 문자를 사용하면 됩니다. 다음과 같이 말이죠.
import React from 'react';
type GreetingsProps = {
name: string;
mark: string;
optional?: string;
};
function Greetings({ name, mark, optional }: GreetingsProps) {
return (
<div>
Hello, {name} {mark}
{optional && <p>{optional}</p>}
</div>
);
}
Greetings.defaultProps = {
mark: '!'
};
export default Greetings;
만약 이 컴포넌트에서 특정 함수를 props로 받아와야 한다면 다음과 같이 타입을 지정 할 수 있습니다.
import React from 'react';
type GreetingsProps = {
name: string;
mark: string;
optional?: string;
onClick: (name: string): void; // 아무것도 리턴하지 않는 함수를 의미합니다.
};
function Greetings({ name, mark, optional, onClick }: GreetingsProps) {
const handleClick = () => onClick(name);
return (
<div>
Hello, {name} {mark}
{optional && <p>{optional}</p>}
<div>
<button onClick={handleClick}>Click Me</button>
</div>
</div>
);
}
Greetings.defaultProps = {
mark: '!'
};
export default Greetings;
그러면, App에서 해당 컴포넌트를 사용해야 할 때 다음과 같이 작성해야 합니다.
import React from 'react';
import Greetings from './Greetings';
const App: React.FC = () => {
const onClick = (name: string) => {
console.log(`${name} says hello`);
};
return <Greetings name="Hello" onClick={onClick} />;
};
export default App;
또는 (밑에는 React.FC를 생략한 코드입니다.)
import React from 'react';
import Greetings from './Greetings';
function App() {
const onClick = (name: string) => {
console.log(`${name} says hello`);
};
return <Greetings name="Hello" onClick={onClick} />;
};
export default App;
이제 작동이 잘 되는지 확인해보세요.
TypeScript를 사용하신다면 만약 여러분이 컴포넌트를 렌더링 할 때 필요한 props를 빠뜨리게 된다면 다음과 같이 에디터에 오류가 나타나게 됩니다.
(name props가 빠뜨려진 상태입니다.)
그리고 만약 컴포넌트를 사용하는 과정에서 이 컴포넌트에서 무엇이 필요했더라? 하고 기억이 안날때는 단순히 커서를 컴포넌트 위에 올려보거나, 컴포넌트의 props를 작성하는 부분에서 Ctrl + Space 를 눌러보면 됩니다.
이제, 함수형 컴포넌트를 작성하기 위한 기본적인 방법의 공부를 끝마쳤습니다.
이번 시간에 공부한 것들을 요약해보자면, 다음과 같습니다.
다음에는 타입스크립트 함수형 컴포넌트에서 상태관리를 하는 방법을 공부해보도록 하겠습니다.