본문 바로가기

프로그래밍 공부내용/타입스크립트(TS)

타입스크립트는 만능이 아니다(feat 리액트 컴포넌트 타이핑하기)

안전한 개발은 매우 중요하다.

 

에러가 터졌을 때 뜨는 빨간 라인은 마치 들불이 번지는 것 처럼 개발자의 마음을 아프게 한다.

 

하.지.만.

 

더 마음 아픈것은 서비스중에 예기치 못한 에러로 불이 나는 것일 것이다.

 

오류는 일종의 불이다. 한 곳에서 장애가 나더라도 번지지 않도록 막는 것이 아주 중요하다.

 

이를 막기위해서 사람들이 여러가지 도구를 고안해냈고 핫 한 도구중 한 가지는 타입스크립트다!!

 

타입스크립트(TS)는 일종의 맞불처럼 미리 빨간불을 띄워줘서 (이것도 마음 아프긴 하다) 여러 오류들을 막아줄 수 있다.

 

과연 타입스크립트가 나를 오류로부터 자유롭게 해줄 수 있을까?

 

PropTypes??

 

리액트에는 TS가 등장하기 전 컴포넌트의 타입을 명시하기 위해서 PropTypes라는 것이 존재했다. 아 물론 현재도 존재한다.

//공식문서 예제 https://reactjs.org/docs/typechecking-with-proptypes.html

import PropTypes from 'prop-types';

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

Greeting.propTypes = {
  name: PropTypes.string
};

다음과 같이 어떤 props가 들어갈지 미리 알려줄 수 있었다.

 

TS와 PropTypes의 차이

땡쿠팀과 지원근로팀은 prop types 대신에 타입스크립트를 사용했다.

 

둘의 가장 큰 차이는 런타임 체크입니다.

 

propTypes는 런타임에서 타입체크를 진행하고 TS는 컴파일타임에서 타입을 체크한다.

 

같이 존재해도 상관없지만 다음과 같은 이유로 propsTypes 없이 사용하고 레거시는 TS로 마이그레이션 했다.

 

1. 컴파일타임에 대부분의 타입에러를 거를 수 있다.

2. 타입은 문서로써의 역할도 한다. 두개를 같이쓰는 것은 중복일 수 있다. 이는 코드볼륨을 키우고 유지보수를 힘들게 한다.

3. 런타임 타입체크는 API와 같이 런타임에서 값이 정해지는 비동기성 작업인데 react-query나 axios가 최근에는 type을 generic 하게 작성할 수 있다. (zod와 같은 라이브러리가 동시에 해결 하능하다고 한다. 쓸일이 있었으면 좋겠다)

4. 문법의 차이. optional등 특정 속성은 공통으로 존재하지만 컴포넌트 prop이외의 타입도 재사용 가능하게 분리할 수 있는 점, 타입을 변수로 재사용 가능한 점이 있어서 TS가 더 유리하다.

 

타입체크(tsc, type checker)

tsc는 타입스크립트 컴파일러로써 컴파일 시에 타입을 체크해준다.

 

tsc를 실행하면 컴파일과 타입검사를 동시에 실행할 수 있지만, fork-ts-checker-webpack-plugin을 사용하면 타입검사를 별도로 실행한다.

 

웹팩 실행시에 타입체크를 따로 실행할 수도 있다는 걸 보면 이 과정이 분리 돼 있는 것이 더 이해가 간다.

 

5개로 분리 돼 있다(Scanner.ts / Parser.ts / Binder.ts / Checker.ts / Emitter.ts)

 

AST + Symbols -> checker -> validation의 순서로 실행된다.

 

웹팩을 실행시 컴파일과 번틀링만 빨리 실행하고 check와 validation은 후에 실행할 수도 있다.

 

이전에 작성했던 웹팩 최적화하기에서 설명했던 babel preset과 ts-loader를 기억해보자

 

babel-loader는 ts->js를 해주고 ts-loader는 컴파일 타입체크와 변환을 둘 다 할 수 있다.

 

컴파일과 런타임에서의 타입 차이

컴파일 단에서는 확정할 수 없는 값이 런타임에 있다.

 

api 같은 경우 런타임에서 타입이 결정된다. 타입체커나 웹팩이 컴파일 단계에서 타입을 확신할 수 없다.

 

이럴 경우 타입가드를 통해서 타입을 확정해주는게 좋다.

 

애초에 런타임과 컴파일타임을 고려해서 타입을 작성하는 것도 좋다.

 

땡쿠팀에서는 쿠폰관련 타입을 작성할 때 타입추론을 잘 못 하는 경우가 있었다.

 

TS는 타입을 암묵적으로 추론할 수 있다. 하지만 잘 추론하지 못하는 경우가 있는데,

 

const coupon = {
	kind:'coffee' //coffee | meal
        message:'사랑해요~'
}

을 추론하게 되면

coupon {
	kind:string,
        message:string,
}
으로 추론하게 된다.

틀린 추론은 아니지만 우리가 사용하고 싶은건 kind 에 'coffee | meal'이다.

 

이럴때 as const 등을 사용해서 명시해줄 수 있다.

 

추가적으로 Type Assertion이나 any를 이용해서 타입을 작성하면 컴파일은 넘어갈 수 있지만 런타임에 에러를 뽑아내게 된다.

 

최대한 정확하고 좁은 타입을 작성해서 이런 오류를 막아주는 것이 좋다.

 

컴포넌트 타이핑하기

최대한 좁게 타이핑 하는 게 안전하다.

 

jsx.Element와 ReactElement<T> ReactNode 순으로 넓다고 볼 수 있다.

 

//선언부를 따라가 보면 아래와 같이 작성된 것을 볼 수 있다.
type ReactNode = ReactElement | string | number | ReactFragment | ReactPortal | boolean | null | undefined;

namespace JSX {
	interface Element extends React.ReactElement<any, any>
  }

 

만일 childeren으로 들어올 값이 string이라던지 명확히 정해진 값이라면 명시해주는 것이 좋은 것 같다.

 

땡쿠팀에서는 가능한한 좁은 타입을 선언하고 prop에 대한 type을 파일 상단부에 따로 분리해 주는 식으로 사용했다.

 

prop을 inline으로 작성하는 것에 비해서 가독성이 좋고, 타입선언을 하기 때문에 재사용이 가능하다.

 

type MyComponentProps = {
  MyItems: MyItem[];
  comment: string;
  D?: React.MouseEventHandler<HTMLButtonElement>;
};

const MyComponent = ({}: MyComponentProps) => {
	...
}

API 타이핑하기

팀에서는 리액트쿼리를 사용했기 때문에, 리액트 쿼리를 기준으로 설명하도록 하겠습니다.

 

기본적으로 Generic을 많이 사용합니다.

 

공식문서 를 보면 예시가 잘 나와있다.

 

const { data } = useQuery<MyType>('groups', fetchGroups) // data type은 Mytype | undefined

과 같이 선언하면 type을 줄 수 있다.

 

return type이 MyType | undefined였기 때문에 'isSuccess' 상태를 통해서 타입가드를 이용해서 처리해줘야합니다.

 

이와 관련해서 destruct형태를 사용하지 않거나 하는 방식으로 하면 편하다는 논의를 발견했지만, 실제 적용했을 때는 소득이 없었습니다.

 

현재로써는 useQuery자체에서 처리하는 매력적인 방법은 없었던 것 같습니다..

(query내에서 error를 추가로 throw처리하는 방법도 고려했으나, isError도 이미 존재하고있고 fetch실패가 아닌경우도 있어서 오히려 매력적이지 않았습니다)

 

프로젝트에서는 data가 빈 값이라도 도착해야하는 경우에는 값을 직접 내려주었고, 아닌경우에는 initial value생성하는 방식으로 해결하려고 노력했습니다. 

 

같은 문제로 TanQuery에서도 같은 논의가 이뤄지고 있었지만 큰 진전은 없었습니다. 저희가 사용한 방법과 동일한 방법을 예시로 들어주고 있었습니다.

 

function useToDoItems() {
    const fetcher = async () => {
        return (await axios.get<ToDoItem[]>("/todos")).data;
    }

    const queryResult = useQuery(["todos"], fetcher);

    // Handle the undefined case by giving some "initial data"
    return { todos: queryResult.data ?? [], ...queryResult }
}

 

만능은 아니다.

propTypes를 TS로 마이그레이션하고, 컴포넌트와 변수에 타입을 달면서 굉장히 즐거웠다.

 

각 요소들이 어떤 형태를 가지고 있는지 알기가 쉽고 특히나 vscode에서 실시간으로 컴파일을 해서 자동완성을 해주는 기능은 정말 짜릿했다.

 

하지만 TS의 한계는 컴파일 단계라는데 있다.

 

어찌됐건 Api를 찌른후 response를 써야하는 프론트 입장에서는 TS 뿐만 아니라 최대한 안전하게 작성할 수 있는 다른 도구를 마련해놓는 것이 유리한 듯 하다.