: 새로 맡게된 프로젝트가 JS로 돼있어서 이를 TS로 바꾸고자 한다. 이유는 직접 TS를 써보고 난 후에 든 주관적인 생각이지만, TS를 쓰면 오히려 복잡한게 많고, 제한도 많지만, 이에 따라 런타임에서 일어날만한 에러 요소들을 먼저 해결하고, 알아서 집어준다는 점에서 유용한 점이 많다는 판단에서 이다.
: 먼저, typescript와 관련된 그리고 타입스크립트 자체를 npm에서 다운받아준다. 나는 npm을 쓰고 있으므로
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
yarn add typescript @types/node @types/react @types/react-dom @types/jest
설치를 마쳤으면, tsconfig.json 파일을 만들어주는데, 직접 만들기 보다는
npx tsc --init
이렇게 만들어준다.
: 앞서 만든 tsconfig.json 내부에는 기본으로 여러가지 설정들이 주석처리 되거나 입력돼 있다. 그럼 내 프로젝트는 어떤 tsconfig 설정으로 사용하는게 좋을까?.
아래가 내가 적용한 옵션이고, 여기서는 내가 설정한 옵션들 중 몇가지만 다루도록 하겠다.
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@stores/*": ["src/stores/*"],
"@hooks/*": ["src/hooks/*"],
"@routes/*": ["src/routes/*"],
"@utils/*": ["src/utils/*"]
},
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "react-jsx",
"module": "esnext",
"allowJs": true,
"baseUrl": ".",
"outDir": "./dist",
"moduleResolution": "Bundler",
"strict": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
},
"include": ["./src/**/*"],
"exclude": ["node_modules", "dist"]
}
strict : true
를 해주면 자동으로 {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
}
위의 옵션들도 true가 되기 때문에 나의 경우 굳이 입력해주진 않았다.
import LoginForm from "src/components/page/login/form";
이런식으로 컴포넌트를 임포트하는 부분이 있다고 하자. 조금 과장한거긴 하지만 이렇게 모든 경로를 쓰게 되면 코드가 지저분해 보이고, 일일이 치는 것도 일이다. 하지만 이걸 조금이나마 줄일 수 있는데 이걸 paths가 해준다. paths에 "paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@stores/*": ["src/stores/*"],
"@hooks/*": ["src/hooks/*"],
"@routes/*": ["src/routes/*"],
"@utils/*": ["src/utils/*"]
},
위와 같이 해주면 된다. 그러면 앞서 말한걸 import LoginForm from "@components/page/login/form";
이렇게 줄일 수 있다. 좀 더 줄이고 싶으면 위의 로직을 이해한 다음에 더 줄여서 쓰면 된다. 추가로, 나는 프로젝트를 쓸 때 vite를 썼는데, vite.config.js
에서도 resolve 프로퍼티에
resolve : {
alias: [
{ find: "@", replacement: path.resolve(__dirname, "./src") },
]
}
위와 같이 alias를 줄 수 있다. ts 와 vite 를 같이 쓸 때는 tsconfig와 vite config 둘다에 입력을 해줘야하는데, 귀찮은 작업이므로 tsconfig에 한번 작업해두면, 자동으로 vite config에서도 인식하도록 tsconfig의 paths를 적용시켜주는 플러그인이 있다.
npm install -D vite-tsconfig-paths
위와 같이 설치해주고
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import tsconfigPaths from "vite-tsconfig-paths";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths()],
});
위와 같이 플러그인에 등록해주면, tsconfig에만 세팅을 해줘도 정상적으로 동작을 한다.
"target": "es5",
나의 경우 이렇게 es5로 해줬다. 사실 대부분의 브라우저는 ES6를 지원하기 때문에 ES6로 지정해줘도 무리는 없지만, IE 호환성 등 구버전 브라우저를 생각하여 ES5를 선택했다.
"module": "esnext",
이렇게 해줬는데 import, export와 같은 최신 모듈 쓰겠다는 뜻
으로 보면 된다. 예를 들어,
// CommonJS
const zip = require("./ZipCodeValidator");
// ES2020
import { valueOfPi } from "./constants";
위와 같은 차이가 있는 것이다.
"moduleResolution": "Bundler"
위와 같이 해줬는데, Bundler 옵션은 TS 5.0 이상부터 쓸 수 있는 나름 신속성(?)이다. 어떤 익스텐션도 쓸 수 있도록 해주며 대신 조건이 있다.
아래는 Bundler 세팅의 장점을 가져와봤다.
From how I'm reading it, the advantage to using this setting is that if your application is configured to transpile using Vite, esbuild or another bundler, TS will use the bundlers resolution strategy to resolve modules.
That means fewer configuration or breaking edge cases, as TS doesn't always play well when we add extra tooling, such as a bundler, in the mix.
In other words this is like "come on TS, trust me bro" but for module resolution.
Granted, I don't know much about the basics of module resolution so I could be inferring something that's wrong.
"esModuleInterop": true,
위와 같이 true로 설정했는데, 이렇게 true로 설정해놓게 되면,
import difference from 'lodash/difference';
difference();
위와 같은 코드를 치게 되면
TypeError: difference_1.default is not a function
위와 같은 에러가 나게 된다. 이는 lodash 라는 모듈에서 CommonJS 스펙의 require를 쓰고 있고, 이에 따라, 위의 코드처럼 ES6 모듈 코드 베이스(import, from)로 CommonJS 모듈을 가져오려고 하면 에러가 나는 것이다. 이 때, esModuleInterop 속성이 위의 코드 처럼 true로 설정될 경우, ES6 모듈 사양을 준수하여 CommonJS 모듈을 가져올 수 있게 되는 것이다. 따라서, 해당 옵션을 쓰면 아래와 같이 트랜스파일링이 되어 정상적으로 동작하게 된다.
// Before
import difference from 'lodash/difference';
difference();
// After
const difference = __importDefault(require('lodash/difference'));
difference();
이에 더하여, 실제로 tsconfig 옵션에는 쓰지 않았지만 allowSyntheticDefaultImports
"allowSyntheticDefaultImports": true,
도 있는데, esModuleInterop: true
로 해놓으면 자동으로 위에처럼 이 옵션도 true로 된다해서 굳이 따로 지정하지 않았다. allowSyntheticDefaultImports
는 본래 default export를 써야지만
import * as someModule from "someModule"
이렇게 안하고
import someModule from "someModule"
이렇게 쓸 수 있었던거를, 반드시 default export를 하지 않아도
import someModule from "someModule"
이렇게 쓸 수 있도록 해준다.
: 사실 이전에는 이렇게 되면 그냥 일단 머지하고,, 후회하는 쪽으로(?) 했는데
https://velog.io/@mayinjanuary/git-%EB%A1%9C%EC%BB%AC%EC%97%90%EC%84%9C-commit-%ED%95%9C-%EB%82%B4%EC%97%AD-%EC%83%88%EB%A1%9C%EC%9A%B4-%EB%B8%8C%EB%9E%9C%EC%B9%98%EB%A1%9C-%EC%98%AE%EA%B8%B0%EA%B8%B0
이 블로그를 통해 방법이 있음을 알게 됐고, 유용하게 써먹었다..