tRPC를 쓰면 타입추론이 더 잘됩니다.
더 잘보다는 그냥 타입을 그대로 쓸 수 있다가 더 좋은 표현인 것 같습니다.
예를들면 다음처럼 추론할 수 있습니다.
하나하나 포인트를 쪼개면서 확인해보면
위처럼 라우터에서 정의한 user를 바로 사용할 수 있고
user에서 정의한 router인 hello와 getUser를 바로 사용할 수 있습니다.
user controller에서 정의한 query를 추론하고, 그 input으로 zod를 통해 정의한 type과 name 역시 바로 추론해주는 것도 확인할 수 있습니다.
놀라운 것은 아래처럼 string union 역시 제대로 추론해주는 부분이라고 생각합니다.
그렇다면 tRPC의 타입 추론은 무한하냐? 라고하면 그렇진 못합니다. 왜냐면 HTTP 응답에서는 Date타입이 아닌 string 타입으로 내려주기 때문입니다.
아래의 상황을 보면 getCurrentTime이라는 새로운 api를 추가할 수 있는데
확인할 수 있는 것 처럼 currentTime은 new Date()를 사용했기 때문에 Date타입으로 추론되야할 것 같지만 string 타입으로 추론됩니다.
실제로 console에 log를 찍어보면
위와같이 코드를 작성했을 때
Date타입은 '2024~~'이렇게 따옴표안에 들어오는 것이 아닌 2024~ 이런식으로 들어와야하지만 그렇지 않게 들어오고, typeof에 찍힌 것 처럼 string 타입으로 들어오는 것을 알 수 있습니다.
tRPC 공식문서에도 나와있는 사항이고,
이는 HTTP 응답에서 Date타입은 string타입으로 내려오는 것이고, 그 이유는 JSON으로 변환하는 과정에서 JSON객체가 Date를 제대로 반환하지 못하기 때문입니다.
그래서 superjson이라는 Date타입도 파싱해주는 라이브러리로 transform해줄 필요가 있습니다.
그래서 우리는 NestJS와 React에 모두 superjson을 transformer로 넣어줘야합니다.
React에 넣을때는 문제가 없지만, NestJS에 넣을때는 문제가 생깁니다.
이는 NestJS가 동작하는 node환경에서 주로 사용되는 module 방식인 commonjs와, superjson의 module방식인 esm 사이에서 발생하는 문제입니다.
이를 해결하기 위해서 결국에는 superjson모듈을 수정하기로 했습니다.
솔직하게는 다른 방향으로 해결하지 못해서 결국에는 그런 선택을 하게 된 것입니다.
다양한 시도를 해봤습니다.
이 부분은 시도해봤지만 실패했고, 실제로 NestJS에도 esm지원에 대한 이야기가 나왔지만 지원이 되는 데 까지는 시간이 더 많이 필요한 것 같습니다..
위는 NestJS의 메인 컨트리뷰터가 쓴 최근 esm에 대한 요구사항 이슈에 대한 응답이고, ESM-only package인 superjson은 사실 class-validator를 더 메인으로 사용하는 NestJS에게는 사실 필요없을 수도 있겠다는 생각도 했습니다.
이 부분도 열심히 시도해봤지만 실패했습니다. 기본적으로 트렌스파일링을 더 깊게 공부하지 못한 것이라 생각해서 조금 더 공부해볼 생각입니다.
마지막 선택지였지만 나름 합리적이라고 생각했습니다.
superjson은 2023년 11월 이후로 버전을 올리지 않았고, 이 버전이 충분히 안정적이라고 생각했습니다.
그리고 NestJS, 특히 node환경이라는 특징은 굳이 esm을 사용하지 않아도 충분히 안정적이지 않을까 생각했습니다.
esm을 사용할 때의 가장 큰 장점인 tree shaking이 서버 환경에선 큰 의미 없기도하고, cjs가 런타임에서 조금 더 빠를 수 있는 것이 그 이유라고 생각했습니다.
그래서 superjson을 개인 깃허브에 fork 해와서 아래처럼 module을 commonjs로 변경해줬습니다.
이 과정에서도 patch-package를 사용할까 했지만, 변경사항에 이후에도 크게 대응해주지 않을 것 같고, 이 부분이 조금 더 깔끔하다고 생각하여 이렇게 사용했습니다.
그렇게 transform 설정을 해주면 아래와 같이 Date타입을 추론할 수 있게 됩니다.
그럼 이제 콘솔에서도, 타입추론에서도 정확한 Date를 가져올 수 있게 됩니다.
사실 이렇게 됐을 때 가장 큰 이점은, 보다 자유롭게 tRPC를 쓸수 있다는 것에 있습니다.
개인적으로 유저 정보처럼 활용도가 높은 데이터의 경우 이를 react-query로 관리할 수도 있지만 zustand 같은 상태관리 라이브러리를 통해 관리하는 것을 선호합니다. 이 때는 서버에서 받아온 데이터를 기반으로 type을 만들어줘야하는데
이때 서버에서 자동으로 정의된 schema 타입을 보면 Date타입이 캐스팅 되는것을 알 수 있습니다.
(개인적으로 orm을 prisma를 사용하고 nestjs-zod-prisma 라는 라이브러리를 통해 타입을 자동으로 만들어줍니다.)
그래서 사용할 때도
서버에서 바로 가져온 User라는 타입을 사용할 때 ts에서 에러를 만들지 않지만,
이 때 superjson을 사용해서 트랜스폼하지 않는다면 아래처럼
tRPC에서는 string으로 판단하고, schema에서는 Date로 판단하기 때문에 에러를 만나게 됩니다.
사실 tRPC와 Nest랑 궁합이 엄청 잘맞다고 생각되진 않지만, 이렇게 문제를 만나고 해결해가면서 더 다양한 개념들을 이해할 수 있게된 것 같습니다.