Today I Learend

JS & React -> TS & React로 리팩토링

TS로 리팩토링 하는 이유

: 새로 맡게된 프로젝트가 JS로 돼있어서 이를 TS로 바꾸고자 한다. 이유는 직접 TS를 써보고 난 후에 든 주관적인 생각이지만, TS를 쓰면 오히려 복잡한게 많고, 제한도 많지만, 이에 따라 런타임에서 일어날만한 에러 요소들을 먼저 해결하고, 알아서 집어준다는 점에서 유용한 점이 많다는 판단에서 이다.

TS 리팩토링 사전 단계

: 먼저, typescript와 관련된 그리고 타입스크립트 자체를 npm에서 다운받아준다. 나는 npm을 쓰고 있으므로

npm
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
yarn
yarn add typescript @types/node @types/react @types/react-dom @types/jest

설치를 마쳤으면, tsconfig.json 파일을 만들어주는데, 직접 만들기 보다는

npx tsc --init

이렇게 만들어준다.

tsconfig 설정

: 앞서 만든 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"]
}
  • noImplicitAny : 변수들이 미리 정의된 타입을 가져야 하는지 여부를 설정하는 옵션이다. 타입스립트는 타입 정보를 가질 때 타입스크립트의 장점을 발휘할 수 있기 때문에, 되도록이면 true로 설정해줘야 한다. 타입 에러를 사전에 발견하기 수월해지고, 코드 자동완성을 통해 생산성이 향상된다. 이 때, strict : true 를 해주면 자동으로
{
  "noImplicitAny": true,
  "strictNullChecks": true,
  "strictFunctionTypes": true,
  "strictBindCallApply": true,
  "strictPropertyInitialization": true,
  "noImplicitThis": true,
  "alwaysStrict": true,
}

위의 옵션들도 true가 되기 때문에 나의 경우 굳이 입력해주진 않았다.

  • paths : import를 할 때 from 부분에 경로를 쓸 때 좀더 깔끔하게 쓰기위해 쓰는 옵션이다. 예를 들어, 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 : 해당 TS 파일을 어떤 JS 버전으로 변환해줄지 결정하는 속성이다.
"target": "es5",

나의 경우 이렇게 es5로 해줬다. 사실 대부분의 브라우저는 ES6를 지원하기 때문에 ES6로 지정해줘도 무리는 없지만, IE 호환성 등 구버전 브라우저를 생각하여 ES5를 선택했다.

  • module : 자바스크립트 파일 간 import를 할 때 어떤 문법을 사용할지 결정하는 속성이다(어떤 문법으로 트랜스파일링 할지).
"module": "esnext",

이렇게 해줬는데 import, export와 같은 최신 모듈 쓰겠다는 뜻으로 보면 된다. 예를 들어,

// CommonJS
const zip = require("./ZipCodeValidator");

// ES2020
import { valueOfPi } from "./constants";

위와 같은 차이가 있는 것이다.

  • moduleResolution : 어떤 알고리즘으로 모듈을 찾고, resolve할지를 결정하는 속성이다.
  "moduleResolution": "Bundler"

위와 같이 해줬는데, Bundler 옵션은 TS 5.0 이상부터 쓸 수 있는 나름 신속성(?)이다. 어떤 익스텐션도 쓸 수 있도록 해주며 대신 조건이 있다.

  • tells TypeScript that you’re code will be bundled by another tool, and thus to loosen the rules with imports (can have no extension, or use .ts extensions)
  • requires to use module set to es2015 or later (which enables parsing exports in package.json and other changes)

아래는 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
"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
이 블로그를 통해 방법이 있음을 알게 됐고, 유용하게 써먹었다..

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글