Total Typescript - HOC

김동하·2025년 4월 11일

typescript

목록 보기
12/21
post-thumbnail

1. HOC

코드

리액트에서 자주 사용되는 HOC 패턴의 타입을 지정해보자!

여기 WrappedComponent가 있다. WrappedComponentUnwrappedComponentwithRouter의 컴포지션이다.

UnwrappedComponent 단순히 routerid를 props로 받는다.

withRouter는 컴포넌트를 받아 router를 주입한 새로운 컴포넌트를 반환한다.

문제

이제 WrappedComponent를 사용해보면 여기서 몇 가지 문제점이 발견된다.

먼저 WrappedComponentrouter를 주입받기 때문에 props로 필요가 없는데도 불구하고 router를 props로 내려도 에러가 발생하지 않는다

그리고, UnwrappedComponent의 props 중 id는 string임에도 number 타입을 내려도 에러가 발생하지 않는다.

이러한 문제를 해결해보자.

withRouter HOC 함수의 타입

HOC 함수에서 아우터 함수 (Component를 props로 받는)와 이너 함수(props를 props로 받는)의 타입을 각각 정해줘야 한다.

props 경우 제네릭이므로 TProps제네릭으로 만들고 Component는 그 제네릭한 props 를 인자로 받아 ReactNode를 반환한다.

TPropsReactNode 추가해준다.

omit

그럼 타입스크립트는 router 타입이 필요하다는 추론을 해준다. 하지만, WrappedComponent의 경우 router props는 필요하지 않다.

omit을 추가해주면 <Component/>에서 장황한 에러를 발생시킨다.

즉, TPropsrouter 프로퍼티를 포함한다는 것을 타입스크립트가 명시적으로 알지 못하기 때문에 발생한다.

단계별로 살펴보면 Omit<TProps, "router">TPropsrouter가 없는 경우 아무 작업도 하지 않는다.

그렇기 때문에 {...props} + router={router} 조합 시 TProps에 이미 router가 있을 수 있다고 타입스크립트는 판단하는 것이다.

즉, 개발자가 as를 통해 타입을 강제해야 하는 상황이다.

이제 에러를 해결했다.

displayName

위 에러를 해결하기 위해서는 React.FC 혹은 ComponentType을 타입을 사용해야 한다.

해결

이제 원하는대로 타입 체킹을 잘 해주게 된다.

UnwrappedComponent의 경우 router 프롭스가 필요하다.

WrappedComponent의 경우 router 프롭스가 없어도 된다.

props의 타입도 잘 체킹해준다.

2. HOC with generic

새로운 예제

이제 아까 만든 withRouter를 사용해본다고 가정하자. 흔히 볼 수 있는Table 컴포넌트다

테이블 컴포넌트는 TableProps타입을 받아서 table을 렌더링한다.

1번 예제와 동일하게 withRouter로 맵핑해주고

이렇게 사용한다.

문제

역시나 문제가 생긴다.

그냥 <Table/>를 사용했을 때 (1번 예시에서의 UnwrappedComponent같이 withRouter 없이) router를 HOC로 주입해주는데도 router가 필요하다는 타입 에러가 발생하고

row의 타입을 추론하지 못하는 것!

그럼 늘 그렇듯 해결에 나서보자

구조적 타입 시스템의 한계

반환 타입으로 ComponentType을 사용한 이유는 displayName의 타입이 React.ReactNode에 없어서였다.

하지만, 타입스크립트에서 함수에 속성을 추가하면 호출 시그니처 + 객체 속성의 교차 타입으로 추론한다.

간단히 설명하자면

  1. 기존 함수 타입인 (props: TProps) => ReactElement

  2. displayName이라는 속성을 추가

  3. { (props: TProps): ReactElement; displayName: string } 객체 리터럴 타입으로 추론함

  4. 반환된 NewComponent함수 + displayName 속성을 가진 객체로 추론

  1. 여기서 객체 타입으로 추론되면서 TProps와 연결이 끊어지는 제네릭 컨텍스트 손실이 일어난다. 그래서 unknown으로 타입이 추론되는 것이다.

이 로직 중에서 헷갈렸던 부분이 ComponentType에는 displayName 속성이 있는데 왜 합쳐서 객체로 추론해버릴까였다.

찾아보니 이는 구조적 타입 시스템의 한계라고 한다. 즉, 타입스크립트는 값의 형태에 타입을 의존하기 때문에 수동으로 속성을 추가하면 새로운 객체 타입으로 추론하면서 기존의 타입을 인식하지 못한다고 한다. (와 진짜 어렵네)

정리하자면

  1. 타입스크립트의 구조적 타입 시스템 때문에 발생

  2. 타입스크립트는 값의 형태(Shape) 에 집중

  3. 함수에 속성이 추가되면 새로운 형태의 객체로 인식

  4. 이 과정에서 제네릭 타입 매개변수(TProps) 와의 연결이 유실

그럼 이제 Component의 타입을 ReactNode로 변경해준다.

그리고 withRouter의 타입을 확인해보면 unknown과 여전히 리턴 타입이 객체임을 확인할 수 있다. (제네릭이 소실된다)

해결

가장 간단한 방법은 as

Component에 명시적으로 as를 추가해주는 것이다.

이제 withRouter는 제네릭을 잃지 않고도 내부 반환 컴포넌트에 속성을 추가할 수 있게 되었다.

참고 : total typescript

profile
프론트엔드 개발

0개의 댓글