우당탕탕 현업에서의 타입스크립트 적용기

HumbleMincho·2024년 7월 8일
post-thumbnail

현업기록소 태그가 있는 포스팅들은 내가 실제로 업무를 하며 팀 내 위키에 공유용으로 적은 글을 보안상 문제요소들을 수정하여 옮긴 포스팅이며, 회고 및 복습을 위함이니 참고부탁합니다.

타입스크립트 도입을 해야겠다 마음먹은지 꽤 됐지만 즉시 실행을 못한 게으른 나에 대한 양심고백부터 시작한다. -> 작년 10월에 완료했지만 기록을 지금와서 하는 나도 레전드!

타입스크립트 사용을 고려해야 하는 이유

높은 수준의 코드 탐색과 디버깅

타입스크립트는 컴파일 언어라고 했는데, 바로 이 컴파일 과정에서 오류를 잡아내기 때문에 오류를 잡아내기 쉽다는 장점이 있다. 자바스크립트는 반면, 실제로 동작하는 과정(런타임 환경)에서 오류를 잡아낼 수 밖에 없다. 가령, 개발자가 예전에 만들어놓은 함수에 파라미터 값으로 숫자 타입을 받아야 하는데 한참 뒤에 그 함수를 사용하면서 스트링 타입의 파라미터를 받았을 경우, 자바스크립트는 이것을 잘못됐다고 하지 않는다. 그러나 타입스크립트는 컴파일 단계에서 잘못됐다고 잡아준다.

타입스크립트를 사용하면 '잔소리 많은 엄마가 옆에서 자꾸 잔소리하는 기분'이라고 표현하는 글을 봤는데, 자바스크립트보다 지정해줘야 할 것도 많고 생각보다 귀찮은 작업이 많다. 그러나 결국 타입스크립트를 사용하는 목적을 견고한 앱을 만드는 것이기 때문에 귀찮더라도 세세하게 다 잡아주는 것이 오류를 막는데 도움이 될 수 있다. 또한 코드 자동완성이나 실행 전 피드백을 제공하여 작업과 동시에 디버깅이 가능해 생산성을 높일 수 있다.

강력한 생태계

타입스크립트는 그리 오래되지 않은 언어임에도 불구하고 강력한 생태계를 가지고 있다. 대부분의 라이브러리들이 타입스크립트를 지원하며 마이크로소프트의 비주얼 스튜디오 코드(VSCode)를 비롯해 각종 에디터가 타입스크립트 관련 기능과 플러그인을 지원한다.

점진적 전환 가능

기존의 자바스크립트 프로젝트를 타입스크립트로 전환하는데 부담이 있다면 추가 기능이나 특정 기능에만 타입스크립트를 도입함으로써 프로젝트를 점진적으로 전환할 수 있다. 자바스크립트에 주석을 추가하는 것에서부터 시작해 시간이 지남에 따라 코드베이스가 완전이 바뀌도록 준비 기간을 가질 수 있다.

하지만, 새로운 프로그래밍 언어에 대한 러닝 커브(Learning Curve), 상대적으로 낮은 가독성, 코드량 증가 등의 이유로 타입스크립트 사용을 망설이는 개발자가 많다. 프로젝트 성격에 따라 타입스크립트를 사용할지 결정하면 된다. 프로젝트의 규모가 크고 복잡할수록, 유지보수가 중요한 장기 프로젝트일수록 타입스크립트의 이점이 부각될 것이다. → 더쎈카드에 적합


프론트엔드 프레임워크와 타입스크립트

리액트(React)

리액트와 타입스크립트의 호환성은 좋은 편임.

리액트 공식 홈페이지에서는 타입스크립트를 사용하기 위한 가이드를 제시하고 있고 페이스북에서 공식적으로 배포하는 리액트 웹 개발용 보일러 플레이트(Boilerplate, 구조·설정 작업을 자동으로 진행해주는 도구이자 명령어)인 Create React App은 간단한 옵션 추가만으로 타입스크립트를 사용할 수 있도록 지원한다.

뷰(Vue.js)

뷰 2.0에서는 타입스크립트를 사용할 수 있지만 몇몇 라이브러리의 도움을 받아야 하거나 구현 자체가 안 되는 문제도 다수 있었다. 다행스럽게도 최근 릴리즈된 뷰 3.0부터는 타입스크립트를 공식 지원한다. 뷰 3.0 CLI(Command Line Interface)는 타입스크립트 도구화 지원을 기본으로 제공한다.

앵귤러(Angular)

자바스크립트 기반인 앵귤러 버전 1과 달리 버전 2부터는 타입스크립트 기반으로 만들어졌고 타입스크립트를 권장하고 있다.


타입스크립트로 어떻게 전환하면 좋을까?

자바스크립트 프로젝트를 타입스크립트로 어떻게 전환할 수 있을까? 유효한 자바스크립트는 유효한 타입스크립트이며 타입스크립트는 모든 타입을 명시적으로 선언하지 않아도 최선을 다해 타입을 추론하려고 한다. 이러한 사실은 자바스크립트를 타입스크립트로 점진적, 단계적으로 전환할 수 있음을 의미한다.

1. 비주얼 스튜디오 코드(VSCode)로 자바스크립트 페어링

비주얼 스튜디오 코드에는 편집기 또는 특정 작업 영역(프로젝트)에 대해 자바스크립트에서 타입스크립트 검사를 활성화 할 수 있는 설정이 포함되어 있다. .vscode/setting.json에 다음 코드를 추가할 수 있다.

{"Javascript.implicitProjectConfig.checkJs": true
}

이제 타입이 안전하지 않은 줄에는 에디터에서 해당 코드를 에러로 표시한다. 이러한 에러 메시지는 에디터에만 표시되며 코드에는 영향을 주지 않는다.
@ts-ignore - 해당 줄
@ts-nocheck - 전체
해당 코드를 추가하여 타입 체크를 선택적으로 비활성화 할 수도 있다.

2. 자바스크립트용 타입스크립트 컴파일러 사용

타입스크립트 컴파일러를 설치하고 스크립트를 실행하거나 설정한다. tsconfig.json 파일을 사용하여 컴파일러 설정을 세팅한다.

{"compilerOptions": {
  "allowJs": true
  "checkJs": false
  "outDir": "./dist"
  "rootDir": "./src"
  "strict": false}
}

"allowJs": true //타입스크립트 컴파일러를 통해 실행할 자바스크립트 파일을 선택
"checkJs": false //자바스크립트 파일에 대한 타입 체크

타입스크립트 컴파일러를 실행하고 자바스크립트를 확인한다.
"checkJs": true 타입 체크를 활성화하고 타입 체크를 진행한다. (node.js 경우에는 @types/node 설치.)

3. 자바스크립트 파일을 타입스크립트 파일로 변환

자바스크립트에서 타입이 안전한 파일인지 확인한 후 하나씩 타입스크립트 파일로 변환한다. (.js → .ts) allowJS를 활성화 했으므로 한번에 모두 이동할 필요 없이 점진적으로 이동할 수 있다. 이미 자바스크립트 코드에 대한 타입 체크를 실행했으므로 업데이트가 너무 많지 않을 것이다.

4. 엄격한 타입 체크

타입 체크를 더 엄격하게 하는 몇 가지 옵션이 있다.

{"compilerOptions": {"noImplicitAny": true,"strictNullChecks": true,"strictFunctionTypes": true,"strictBindCallApply": true,"strictPropertyInitialization": true,"noImplicitThis": true,"alwaysStrict": true
}

세팅 순서

1. typeScript 설치

2. 설치 후 tsConfig 설정

3. 설정했더니 styled-component에서 타입에러 → @type의 개념

4. babel.config.js 파일에 module-resolver 에 ts관련 확장자 추가

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    'babel-plugin-styled-components',
    [
      'module-resolver',
      {
        root: ['./app'],
        extensions: ['.ts', '.tsx', '.jsx', '.js', '.json'],
      },
    ],
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    'react-native-reanimated/plugin',
  ],
  env: {
    production: { plugins: ['transform-remove-console'] },
  },
};

5. VSCode Extensions 설치
Pretty TypeScript Errors : typeScript 에러문구를 정렬시키고 하이라이트까지 이쁘게 보여줌ClassDiagram-ts: typeScript 클래스 아키텍쳐를 문서화 해줌
요렇게 원하는 폴더 우클릭 후 show diagram 선택하면
실제로 요런 형태로 보여줌.


타입 관련 배경지식들

내가 TS로 전환하면서 다시한번 읊어야했던 개념들이다.

React.ReactNode, React.Component

자식 요소를 감싸는 래퍼 컴포넌트를 작성할 때 자식 요소인 children 속성의 타입을 명시해야 하는 경우가 자주 있다.
그런데, ReactChild, ReactElement, ReactNode 등 헷갈릴 수 있으니 아래 설명 참고.

const App = () => {
  return (
    <div className="App">
      <Wrapper>
        {/* 과연 children 요소의 타입은 무엇일까요? */}
        <div>Hello, world!</div>
      </Wrapper>
    </div>
  );
};

type WrapperProp = {
  // children: React.ReactChild;
  // children: React.ReactElement;
  // children: JSX.Element;
  // children: React.ReactChildren;
  // children: React.ReactNode;
    // ...으아아아!! ...
};

const Wrapper = ({ children }: WrapperProp) => {
  return (
    <div className="Wrapper">
      <div>{children}</div>
    </div>
  );
};

export default App;

React.ReactNode

제일 처음 알아볼 타입은 ReactNode 타입.
ReactNode는 children 속성의 타입으로 가장 많이 사용하는 타입이다.

// ReactChild 타입에 string, number 타입이 포함.
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

ReactNode 타입은 jsx 내에서 사용할 수 있는 모든 요소의 타입을 의미, 즉 string, null, undefined 등을 포함하는 가장 넓은 범위를 갖는 타입.

TMI : ReactNode 타입은 클래스 컴포넌트 의 render 함수가 기본적으로 리턴하는 타입이기도 함.
반면 함수 컴포넌트는 ReactElement 인터페이스를 리턴.

React.ReactElement

두 번째로 알아볼 타입은 ReactElement 타입.

ReactElement 를 확인해보면 아래처럼 생긴 것을 확인할 수 있다.

interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
    type: T;
    props: P;
    key: Key | null;
}

ReactElement 는 createElement 함수를 통해 생성된 객체의 타입이다.

index.d.ts를 확인해보면 ReactNode가 ReactElement를 포함하고 있음을 알 수 있다.
또한 ReactNode 와는 달리 원시 타입을 허용하지 않고 완성된 jsx 요소만을 허용하는데, 즉 위 사진에서 확인할 수 있는 것처럼 ReactNode 타입이 ReactElement 타입을 포함하고 있는 관계임을 확인할 수 있다.
따라서 위 사진처럼 원시 타입 리터럴을 children 속성으로 사용하려 하면 에러를 출력하는 모습인데, 따라서 ReactElement 타입은 자식 요소로 하나의 "컴포넌트" 를 받는 것을 강제해야 하는 상황에 사용할 수 있다.

createElement 함수의 리턴값

createElement와 ReactElement의 관계도 확인해보자.
jsx로 간단한 태그를 만들어본 다음 바벨을 통해 트랜스파일을 수행해보면

<div name="chanmin">Hello, {name}</div>

아래와 같은 결과물이 출력된다.

"use strict";

/*#__PURE__*/
React.createElement("div", {
  name: "chanmin"
}, "Hello, ", name);

트랜스파일된 결과물(createElement 함수)을 확인해보면 생성한 태그에 대한 정보가 담겨 있는데, 자세히 살펴보면 ReactElement 가 가진 type 과 prop 이 담겨있는 모습이다.

따라서, createElement 함수가 리턴한 모든 객체는 ReactElement 인터페이스를 구현하고 있음을 알 수 있다.

그럼 JSX.Element 와 ReactElement 의 차이는 무엇인가

거의 없다고 보면 됨ㅎㅎ...

// index.d.ts
declare global {
    namespace JSX {
        interface Element extends React.ReactElement<any, any> { }
                ...
        }
}

JSX.Element는 단순히 ReactElement 인터페이스를 상속받았을 뿐이고 내부 구조나 제약 타입이 별도로 존재하지 않아 완전히 동일하다고 봐도 무방하다.

React.ReactChild

마지막으로 알아볼 타입은 ReactChild 타입.

type ReactChild = ReactElement | ReactText;

ReactChild 는 ReactNode 타입을 적절히 내로잉(narrowing)한 타입인데, ReactElement 타입이 리액트 요소 객체만을 허용했다면, ReactChild 타입은 여기서 원시 타입까지 허용하는 타입이다.

TL;DR;

타입별 허용 범위 비교 : ReactNode > ReactChild > ReactElement

  1. ReactNode 타입
    컴포넌트, 원시 타입, null, undefined를 모두 허용하는 타입.
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
  1. ReactChild 타입
    컴포넌트, 원시 타입 리터럴을 허용하는 타입.
type ReactChild = ReactElement | ReactText;
  1. ReactElement 타입
    createElement 함수를 통해 생성된 컴포넌트만을 허용하는 타입.
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
    type: T;
    props: P;
    key: Key | null;
}

겪었던 문제들

1. import에서 자꾸 말썽을 피움.(.tsx)

babel.config.js파일 수정한 상태에서 metro 캐시 초기화 후 build

런타임 쪽에서 .tsx확장자 파일을 js파일로 import할 때 cannot resolve module에러가 발생했는데
babel.config.js파일도 수정해보고 했지만 여전히 똑같은 현상이 발생하여(별 생쑈를 다 했지만)
우여곡절 끝에 metro 캐시 초기화를 했더니 정상적으로 경로를 읽었다.

2. global패턴에서 인식하지 못함

3. prototype 관련 에러

그럴 땐 아래와 같이 app/types/Prototype.d.ts에 정의해줬다. 코드는 flow-typed/ProtoType.js에서 참조했다.

declare global {
  interface Date {
    format(date: string): string;
  }
}

export {};

4. 그냥 삽질

iOS에서 빌드 안되는 현상은 xProject파일의 변경사항이 있는지 봐라. typeScript 종속성은 pod에 영향주지 않는다… 이거때문에 하루 버렸던 기억이 난다.

5. 특정 타입을 import하는 케이스들

아래는 flatList 등 renderItem속성이 있는 요소에서 에러가 나는 케이스다.

ListRenderItem import하고 타입에 적용해라

  import { ListRenderItem } from 'react-native';
  .
  .
  .
  const renderItem: ListRenderItem<CustCardInfo | any> = ({ item }) => {
    .
    .
    .

또 있다. 아래는 FastImage의 source속성에 url적용할 때 나는 뻘건 줄이다.

import { ImageRequireSource } from 'react-native'
  .
  .
  .
  getShopTitle: (imgUrl: ImageRequireSource, shopNm: string) => JSX.Element = (imgUrl, shopNm) => {
    .
    .
    .
    }

위와 같이 react-native에서 import해서 타입명시해야하는걸로 해결


그래서 어떻게 바꿨는가?

  1. global 패턴 모두 제거

  2. Hierarchy 구조 및 import 패턴. → 이건 코드 컨벤션과 직결


앞으로 어떻게 하면 되는데?(후기 및 코드컨벤션)

우선 써보니까… 개요에 설명했듯이 옆에서 엄마가 잔소리하는 기분이 들어서 스트레스 조금은 받지만 누군가가 해당 코드를 참조했을때 api라던지 함수 return값을 직접 해석하고 로그를 찍어보지 않아도 인지적으로 추론이 쉽기 때문에 유지/보수 측면에서는 확실한 이점을 갖고 있다 라는 생각이 들었다.

아래는 앞으로의 코드컨벤션에 영향을 끼칠 수 있는 패턴 및 방식에 대해 설명한다.(지속적 보완 필요)

  1. 기존 글로벌 패턴들은 resolve를 다시 재정비 할거지만 직접 import 또는 require하여 참조하기로 함(현재는 모두 해결 완료)

  2. .d.ts파일은 scenes 하위 1depth 디렉토리 당 하나씩 매칭하여 내부에서만 참조하여 사용하며, common type으로 뺄 일이 있다면 월말 배포/코드리뷰시 define하는 방향으로 진행.(특히 점진적 TS 전환작업 중 소통 필수.) -> 결과적으로 팀 문화 특성상 끊임없는 소통으로 아름답게 해결 완료.


주의사항

  1. 마이그레이션 하면서 파일들이 다 초록색이라고 방심하지 말자

    파일명이 빨간색이면 하나라도 타입관련에러가 존재한다는건데 그렇지 않다고 방심해서는 전혀 안된다. IDE의 TS서버로드가 늦은건지 아니면 나만 그런지 모르겠으나 가만히 있어도 인식이 안될 때가 있다. 마이그레이션할 때 파일 한땀한땀 직접 바라보면서 완벽하게 하나의 파일을 마이그레이션 후 다음 스텝으로 넘어가는 것을 권장한다.

  2. 반대로 빨간 줄 없다고 방심하지 말자!

    위 사진과 같이 f1 또는 cmd+shift+p로 위 명령어를 써주는 것도 알아주자. 뻑날때가 은근 많다.(나만그런가


적용하면서 회고(feat.내가 부족하다고 느낀 항목들)

  1. 자바스크립트 내에서 클래스 개념(interface,…)
  2. import, export 개념(결국 module resolve 개념이라는걸 2024년 초에 깨달았다~)
  3. prototype개념 -> 보충한 포스팅 바로가기
  4. globalThis개념
profile
최고의 효율을 창출하기 위해 겸손히 노력합니다.

0개의 댓글