이번주차 스터디 공유물로 리액트에 점진적으로 TypeScript를 적용하는 방법에 대해서 알아보겠습니다. create-react-app
을 이용해서 리액트 어플리케이션을 만든다면, TypeScript + React 환경을 쉽게 구축할 수 있습니다.
TypeScript를 활용한 리액트 앱을 만들려면, 다음과 같은 명령어를 터미널에 작성하시면 됩니다.
$ npx create-react-app 디렉토리명 --template typescript
이전에 자바스크립트로 create-react-app
을 실행해보신 분이라면, 뒤에 --template typescript
라는 속성 없이 쉽게 리액트 환경을 구축할 수 있었을 텐데요. 디렉토리명 뒤에 ---template typescript
라고 작성해 주면, 우리는 타입스크립트 언어를 기반으로 리액트 환경을 구성할 것이다라고 생각하시면 되겠습니다.
환경설정이 완료되고, cd 디렉토리명
을 터미널에 입력하시고,
npm을 사용하신다면 npm start
,
yarn을 사용하신다면 yarn start
명령어를 실행하시면 됩니다.
vscode에 들어 오시면, 가장 먼저 src
디렉토리 내에 App.tsx
라는 파일이 있을 것입니다.
자바스크립트로 환경을 구성할 때와 달리, 타입스크립트로 환경을 구성하면 리액트 컴포넌트들은 .tsx
라는 확장자 명으로 파일이 구성됩니다.
리액트의 컴포넌트에 타입을 지정해줘야 하는데요. 이전에 hook을 사용하지 못하던 버전들에서는 컴포넌트를 대개 클래스형으로 작성했는데, 최근에는 함수형 컴포넌트로 작성하는 것이 트렌드입니다. 공식 문서에서도 함수형 컴포넌트를 권장하고 있고, 베타 버전으로 따로 함수형 컴포넌트로 공식문서를 작성하고 있을 정도로 함수형 컴포넌트 중심적으로 돌아간다고 말할 수 있을 것 같습니다.
타입스크립트 환경에서는 우리가 작성한 코드들의 표현을 함수형 컴포넌트를 베이스로 하고, 함수형 컴포넌트로 인지되지 않거나 확인되지 않을 경우, 클래스 컴포넌트로 해석한다고 볼 수 있습니다. 기본적으로 우리가 반환값, 즉 return하는 JSX의 결과 타입은 any이지만, JSX.Element 인터페이스를 통해서 원하는 타입으로 변경가능합니다.
클래스형 컴포넌트의 경우 render()
메서드를 통해서 ReactNode
를 반환하는 반면에, 함수형 컴포넌트는 ReactElement
를 반환합니다.
ReactElement는 React.createElement
로 컴파일되고, JSX.element
는 any
타입의 props와 type을 가진 React.createElement
입니다.
ReactNode
>React.Element
Generic >JSX.Element
의 범위로 연관관계를 갖는 다고 생각하시면 쉽게 이해하실 수 있습니다.
FC는 Function Component의 줄임말로, 함수형 컴포넌트의 타입 정의 시에 React.FC
를 타입으로 붙여주면 됩니다.
function App: React.FC<string>{
return (
<div></div>
)
}
React.FC
로 타입을 지정하는 경우에는 props에 기본적으로 children
이 담겨있어, props의 타입을 정할 수 없다는 단점이 있습니다. children
의 요소로 어떤 타입이 들어올 지 예측하기 어렵기 때문입니다. React 18 버전부터는 prop에 대한 타입이 Generic으로 바뀌면서, 직접 props에 대한 타입을 지정해줘야 하기 때문에, React.FC
는 많은 단점을 갖고 있어 사용하는 것을 지양하고 있습니다.
따라서, 쉽게 정리하면 컴포넌트에 대한 타입 React.FC
을 지정하는 대신, 인자로 받는 props에 대한 타입을 interface
또는 type
을 이용해서 선언하고, 해당 props에 타입을 명시하는 방법으로 사용하는 것을 지향해야 한다고 기억하시면 됩니다.
특히, create-react-app을 이용해서 리액트 프로젝트를 생성할 경우
React.FC
의 사용이 어려우므로 사용을 지양합니다.
interface Identification {
name : string;
age: number;
phoneNumber: number;
}
function App(props: Identification){
const { name, age, phoneNumber } = props;
return (
<div>
<h1>{name}</h1>
<p>{age}</p>
<p>{phoneNumber}</p>
</div>
)
}
우리가 일반적으로 자바스크립트에서 함수를 작성하는 방식에는 1) 함수 선언식, 2) 함수 표현식 방법이 있는데요. 그 중에서도 function
키워드를 사용하는 1) 함수 선언식 방법과, 2) 함수 표현식 방법 중에서도 화살표 함수를 활용한 방식을 많이 사용합니다.
리액트에서도 함수형 컴포넌트를 작성 시 화살표 함수로도 사용할 수 있지만, 최근의 트렌드로는 function
키워드를 이용해 컴포넌트를 작성하는 것을 공식문서와 여러 프로젝트들에서 확인할 수 있을 것입니다.
interface Identification {
name : string;
age: number;
phoneNumber: number;
}
function App(props: Identification): JSX.Element{
return (
<div>
<h1>Hello React</h1>
</div>
);
}
쉽게 생각하면, 컴포넌트의 리턴값인 JSX에 대한 타입을 지정하는 방법입니다.
React.FC
의 방법보다는 JSX.Element
의 방식이 더 가독성도 좋고, props에 대한 타입 지정과 분리하여 사용하게 됨에 따라 가독성도 좋고, 개발자 입장에서 코드를 작성하기 훨씬 편리하다고 생각됩니다.
여러 hook들 중에 대표적인 상태 관리 hook인 useState, useReducer, 그리고 비제어 컴포넌트 관리 시 불필요한 렌더링을 방지해주는 useRef hook의 타입 지정에 대해서 알아보겠습니다.
useState를 사용할 때는 Generic
을 사용해서 타입을 지정하는데, 사실 타입스크립트가 타입 추론을 잘 해내기 때문에, 꼭 generic을 사용하지 않고 생략해도 됩니다.
const [number, setNumber] = useState(0);
단, state가 null
일 수도 있고 아닐 수도 있을 때는 Generic
을 사용하는 것이 좋습니다.
또, 단순 원시값이 아닌 참조값을 가리키는 객체나 배열이 상태에 담겨있을 경우 Generic
으로 명시하는 것이 좋습니다.
type Identification = {
name: string;
age: number;
address: string;
}
const [info, setInfo] = useState<Idenfication | null>({
name: 'kyle',
age: 99,
address: 'Seoul',
});
useReducer
를 사용할 때 코드의 구조는 다음과 같습니다.
const [state, dispatch] = useReducer(reducer, initialState);
useReducer
도 useState
처럼 타입 명시를 생략해도 괜찮습니다. 단, 컴포넌트의 외부에 작성될 reducer
함수에서 받아오는 파라미터의 타입과 return 타입을 동일하게 해주는 것이 매우 중요합니다.
type Action = { type: 'plus' } | { type: 'minus'} // union type을 활용하여 타입 별칭으로 다음과 같이 타입을 지정가능합니다.
const initNumber: number = 100;
const reducer = (number:number, action:Action):number => {
switch (action.type) {
case 'plus':
return number + 1;
case 'minus':
return number - 1;
default :
break;
}
}
function App():JSX.Element {
const [number, dispatch] = useReducer(reducer, initNumber);
const plusHandler = () => dispatch({type: 'plus'});
const minusHandler = () => dispatch({type: 'minus'});
return(
<div>
<h1> {number}</h1>
<button onClick={plusHandler}>plus</button>
<button onClick={minusHandler}>minus</button>
</div>
)
}
useRef
는 useState, useReducer와 같이 generic
을 통해 타입을 지정할 수 있습니다.
const ref = useRef<number | null>(0);
generic
을 이용해서 ref.current
를 통해 값을 추론할 수 있습니다.
useRef는 DOM을 특정 값 안에 담을 때(DOM 요소를 직접 제어) 사용하는데, 특히, input에 focus
를 시킬 때 종종 사용합니다.
코드를 통해 살펴보겠습니다.
function App():JSX.Element{
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.target.value = '';
inputRef.current.focus();
}
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef}/>
<button type="submit">로그인</button>
</form>
)
}
위와 같이 inputRef
라는 변수에는 input이라는 DOM이 담길 것인데, 그 전의 초기 상태는 아무런 값도 담겨있지 않으므로, 초기값으로 null
을 지정해주면 됩니다.
추가적으로, event 핸들러 함수인
handleSubmit
은event
라는 합성 이벤트 객체를 파라미터로 받는데, 이 event의 타입도 지정해주어야 합니다. 해당 타입도 generic을 활용해서React.FormEvent<HTMLFormElement>
로 지정해주면 됩니다. vscode가 타입스크립트로 만들어진 코드 에디터이기 때문에, handleSubmit에 마우스를 살짝 올려보면, 타입에 대해서 추론해 잘 알려주곤 합니다!
이렇게 첫 create-react-app
을 통해 타입스크립트 환경에서 리액트 설정을 하는 방법부터, 컴포넌트의 타입 지정, hooks들 중 대표적인 hook의 타입 지정을 하는 방법에 대해서 알아보았습니다.
다음에는 context
API를 활용할 때 타입 지정을 하는 방법과 React Router
에 대해서 정리해 보겠습니다.
[참고자료]