올해 3월, 진행중인 프로젝트를 인수인계 받았다.
타입스크립트와 리액트 기술로 만든 싱글페이지 어플리케이션이다. 주변에 이러한 조합으로 사용하는 이들이 꽤나 있는것 같았고 평소 타입스크립트를 관심있게 보고 있어서 좋은 기회를 얻은 셈이다. 이번 글에서는 타입스크립트로 리액트 개발 환경을 구성하는 방법에 대해 정리해 보겠다.
create-react-app(이하 CRA)으로 프로젝트를 생성했다면 --typescript
옵션으로 개발환경을 빠르게 구현할 수 있다(참고: Adding TypeScript).
하지만 상황에 따라 웹팩을 포함한 모든 개발환경을 직접 세팅해야 하는 경우도 있는데 이번 프로젝트가 그랬다.
그 상황이라는 것은 이런게 아닌가 싶다.
- 요 제너레이터(Yo Generator)로 자동구성한 그런트(Grunt) 기반의 개발환경을 사용했다가 유지보수의 어려움을 겪은 경험이 있었다.
- CRA 업데이트에 따른 유지보수 어려움을 예방하고, 개발환경의 자유도를 높이고 싶다.
이러한 고민의 결과로 직접 개발환경을 만들었을 것이라고 생각한다.
직접 세팅하는 방법
먼저 빈 폴더를 하나 만들고 npm 으로 초기화 한다.
$ npm init -y
package.json 파일이 생성된 것을 확인하고 리액트 패키지를 설치한다.
$ npm install --save react react-dom
자바스크립트에서는 react와 react-dom 패키지만으로 충분했지만 타입스크립트에서는 이들 타입 정보가 필요하다. @types/react와 @types/react-dom이 리액트 패키지에서 사용할 타입이 정의된 패키지다.
타입 정의 파일도 다운 받는다.
$ npm install --save @types/react @types/react-dom
타입스크립트 코드를 작성하기 위해서는 typescript 패키지도 설치해야 한다.
$ npm install --save-dev typescript
다운로드한 typescript 명령어를 이용하면 타입스크립트 설정 파일을 생성할 수 있다.
$ npx typescript --init
초기화를 의미하는 --init
옵션을 붙여 실행하면 tsconfig.json 파일이 자동으로 생성된다.
리액트 jsx 코드를 사용하기 위해서는 compilorOptions의 jsx 속성에 "react" 값을 추가해야 해야한다.
(기본값은 주석 처리 되어 있다)
"compilorOptions": {
"jsx": "react"
에디터(필자가 쓰는 에디터는 VS Code)는 이 tsconfig.json을 참고해서 타입스크립트 문법을 검사한다. 뿐만 아니라 웹팩에서 설정할 ts-loader가 이 파일을 참고해서 트랜스파일 작업을 하기 때문에 tsconfig.json 파일은 먼저 생성해야 한다.
리액트 패키지와 타입스크립트 설정을 마쳤으니 이제는 타입스크립트로 리액트 컴포넌트를 만들 차례다.
컴포넌트 만드는 방법 1
리액트는 컴포넌트를 만들기 위한 세 개의 클래스를 제공하는데 모두 제네릭으로 만들 수 있다.
FC<Props>
PureComponent<Props, State>
Component<Props, State>
컴포넌트의 속성(Props)이나 상태(State) 타입을 받아서 컴포넌트 클래스를 생성한다.
속성만 갖는 함수형 컴포넌트를 만들어 보겠다. src/components/Counter.tsx 파일을 만든다. jsx 코드가 있는 파일을 .jsx로 만드는것 처럼 타입스크립트는 .tsx 확장자로 파일을 만든다.
import * as React form 'react';
const Counter: React.FC<{name: string}> = props => {
const { name } = props;
return <p>{name} counter</p>
}
export default Counter;
{name: string}
타입을 제네릭 인자로 전달했다.
클래스 구현부에서는 인자에 문자열 name이 있다는 것을 보장할 수 있는데, 전달한 props가 모양을 알려 주었기 때문이다.
그렇지 않을 경우 에디터 문법 검사기가 체크할 것이고 타입스크립트 빌드 에러를 발생 시킬 것이다.
개발자는 코드만 보고서도 비교적 쉽게 컴포넌트 속성을 파악할 수 있다는 점 또한 타입스크립트의 장점이다.
속성 타입을 인터페이스로 분리하면 비교적 깔끔한 코드를 유지할 수 있다.
interface CounterProps {
name: string;
}
const Counter: React.FC<CounterProps> = props => {
CounterProps 인터페이스를 정의해서 제네릭에 타입 인자로 전달했다. 컴포넌트에 속성 항목이 많아질수록 이를 인터페이스로 분리하여 정의하는 이점을 더 얻을 수 있을 것이다.
컴포넌트 만드는 방법 2
속성 뿐만 아니라 상태를 갖는 컴포넌트도 만들어 보자.
interface CounterProps {
name: string
}
interface CounterState {
count: number
}
속성 정의 아래 상태 타입을 정의한 CounterState 인터페이스를 만들었다. 리액트 Component나 PureComponent는 상태 타입을 두 번째 인자로 받는 제네릭 함수다. 속성 타입에 이어 상태 타입도 전달하면 상태를 갖는 컴포넌트를 만들 수 있다.
class Counter extends React.Component<CounterProps, CounterState> {
constructor(props: CounterProps) {
super(props)
this.state = {
count: 0,
}
}
componentDidMount() {
setInterval(this.increase, 1000)
}
increase = () => {
const { count } = this.state
this.setState({ count: count + 1 })
}
render() {
const { name } = this.props
const { count } = this.state
return (
<React.Fragment>
<h1>{name} counter</h1>
<div>count value: {count}</div>
</React.Fragment>
)
}
}
컴포넌트가 생성되면(constructor) 상태를 초기화 하고 돔에 마운트되면(componentDidMount) 1초마다 카운트 값을 증가하는(increase) 로직을 작성했다. 컴포넌트를 그릴 때는(render) 프롭스로 받은 카운터 이름과 증가된 상태값을 출력하도록 했다.
타입으로 정의한 CounterState는 count 속성이 있다는 것을 보장한다. 코드에서 잘못 접근할 경우 미리 알수 있는 장점이 있다(가령, 컴포넌트 상태를 초기화하지 않거나 count 변수에 잘못 접근할 경우). 뿐만 아니라 인터페이스 정의만 보더라도 컴포넌트의 모습을 가늠할수 있다는 점이 타입스크립트의 매력이라고 생각한다.
만든 컴포넌트를 엔트리 포인트에 가져와서 붙여 보자.
src/index.tsx:
import * as React from "react"
import * as ReactDOM from "react-dom"
import Counter from "./components/Counter"
ReactDOM.render(<Counter name="React" />, document.getElementById("app"))
웹팩 빌드 설정
타입스크립트로 작성한 코드를 브라우져에서 인식할 수 있는 코드로 변경하기 위해 웹팩으로 빌드 환경을 구성해 보겠다.
먼저 타입스크립트 로더인 ts-loader와 웹팩 패키지를 설치한다.
$ npm install --save-dev ts-loader webpack webpack-cli
webpack.config.js 파일을 만들어 아래 내용으로 웹팩 설정을 구성한다.
module.exports = {
mode: "development",
// 엔트리 포인트
entry: "./src/index.tsx",
// 빌드 결과물을 dist/main.js에 위치
output: {
filename: "main.js",
path: __dirname + "/dist",
},
// 디버깅을 위해 빌드 결과물에 소스맵 추가
devtool: "source-map",
resolve: {
// 파일 확장자 처리
extensions: [".ts", ".tsx", ".js"],
},
module: {
rules: [
// .ts나 .tsx 확장자를 ts-loader가 트랜스파일
{ test: /\.tsx?$/, loader: "ts-loader" },
],
},
}
엔트리 포인트(entry
)를 src/index.tsx로 설정한다.
빌드 결과(output
)는 dist 폴더에 main.js로 만들 것이다.
디버깅을 위해 결과물에 소스맵을 추가한다(devtool
).
모듈이 타입스크립트 파일을 처리하기 위해 resolve.extensions
속성에 .ts, .tsx 확장자를 추가한다.
es6 코드를 바벨이 처리하는 것처럼 타입스크립트 코드를 ts-loader
가 처리하도록 설정했다.
.ts나 .tsx 파일을 ts-loader가 해석하라는 설정이다.
이렇게 웹팩 설정을 마치고 빌드 스크립트를 package.json에 등록한다.
{
"scripts": {
"build": "webpack"
}
}
실행하여면 웹팩이 동작하고 dist 폴더에 main.js과 main.js.map 파일이 생성된다.
$ npm run start
HTML 문서에서 로딩
빌드한 자바스크립트 파일은 html 파일에 로딩되어야 브라우져로 화면을 확인할 수 있다. index.html 파일을 만들어 아래 코드를 작성하자.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React TS!</title>
</head>
<body>
<div id="app"></div>
<!-- 빌드한 자바스크립트를 로딩한다 -->
<script src="./dist/main.js"></script>
</body>
</html>
브라우져로 파일을 열어 확인하면 작성한 리액트 컴포넌트가 렌더링 되는 것을 확인할 수 있다.
자동화
매번 코딩후 빌드하고 브라우져를 갱신하는 작업는 지난한 일이다. webpack-dev-server를 이용하면 이러한 일련의 과정을 자동화 할 수 있다.
$ npm install --save-dev webpack-dev-server
웹팩 설정 중 devServer
속성에 개발 서버 정보를 세팅한다.
devServer: {
contentBase: './',
publicPath: '/dist'
}
index.html이 프로젝트 최상단에 있기 때문에 contentBase
를 './'로 잡는다.
빌드 결과물이 dist 폴더에 있기 때문에 정적 파일을 제공하는 폴더인 publicPath
를 '/dist'로 지정한다.
마지막으로 웹팩 서버 구동을 위한 스크립트를 package.json에 등록한다.
"scripts": {
"start": "webpack-dev-server",
"build": "webpack"
등록한 스크립트로 개발 서버를 실행하면 웹팩 서버가 구동되서 8080 포트에서 확인 할 수 있다.
$ npm start
코드가 변경되면 이를 감지해서 웹팩이 실행행되어 타입스크립트 코드가 트랜파일 되고 8080포트에 접속한 브라우져가 자동으로 갱신될 것이다.
정리
간단하게 타입스크립트로 리액트 개발환경을 만들어 보았다. 자바스크립트에 비해 타입스크립트의 장점은 뭘까?
코드의 가독성이라고 생각한다. 컴포넌트의 속성과 상태를 인터페이스로 정의하기 때문에 비교적 컴포넌트 코드를 수월하게 읽을 수 있다.
뿐만 아니라 에디터에서 실시간으로 문법을 검사하기 때문에 프로그램 실행전에 버그를 잡을 확률이 크다. 타입스크립트를 처음 접할 때는 이러한 문법 체크 기능이 오히려 답답했다. 마치 처음으로 값옷을 입고 무거워서 답답함을 느끼는 것 처럼 말이다. 그러나 언젠가 발생할 런타임 오류를 코딩하는 동안에 예방할 수 있는 것이기 때문에 참고 적응해야 하는 단계라고 생각한다.
이 두 가지가 필자가 경험한 타입스크립트의 장점이다. 아마도 리액트에서 타입스크립트가 해결하려는 문제는 이것 말고도 더 있을지 모르겠다.
참고