TkDodo 의 Type-safe React Query 를 번역한 글입니다
타입스크립트를 사용하는 것이 좋은 생각이라는 것에는 모두 동의할 수 있을 것입니다. 타입 안전을 싫어하는 사람이 있을까요? 버그를 조기에 발견할 수 있는 좋은 방법이며, 앱의 복잡한 부분을 유형 정의로 오프로드할 수 있으므로 머릿속에 영원히 보관할 필요가 없습니다.
type-safety의 수준은 프로젝트마다 크게 다를 수 있습니다. 결국, TS 설정에 따라 모든 유효한 JavaScript 코드는 유효한 TypeScript 코드가 될 수 있습니다. 또한 ‘타입을 갖는 것’과 ‘타입에 안전한 것’ 사이에는 큰 차이가 있습니다.
TypeScript의 강력한 기능을 제대로 활용하려면 무엇보다도 필요한 것이 있습니다:
Trust
우리는 타입을 신뢰할 수 있어야 합니다. 그렇지 않으면 타입은 단순한 제안에 불과할 뿐 정확하다고 믿을 수 없습니다. 그래서 우리는 타입을 신뢰할 수 있도록 그 이상의 노력을 기울입니다.
- 가장 엄격한 타입스크립트 설정을 활성화
ts-ignore
뿐만 아니라any
타입을 금지하는 typescript-eslint 를 추가하고,- 코드 리뷰에서 모든 타입 단언을 지적합니다.
그럼에도 불구하고 우리는 거짓말을 하고 있는 것일 수도 있습니다. 아주 많이요. 위의 모든 사항을 준수하더라도 말이죠.
Generics
제네릭은 타입스크립트에서 필수적입니다. 특히 재사용 가능한 라이브러리를 작성할 때와 같이 원격으로 복잡한 것을 구현하고 싶다면 제네릭을 사용해야 합니다.
그러나 라이브러리 사용자는 제네릭에 신경 쓸 필요가 없는 것이 이상적입니다. 제네릭은 구현 세부 사항이기 때문입니다. 따라서 꺾쇠 괄호를 통해 함수에 일반적인 ‘수동’을 제공할 때마다 두 가지 이유 중 하나로 인해 좋지 않습니다:
불필요한 정보이거나 스스로에게 거짓말을 하고 있는 것입니다.
About angle brackets
꺾쇠 괄호는 코드를 실제보다 “더 복잡하게” 보이게 만듭니다. 예를 들어 useQuery
가 자주 작성되는 방식을 살펴보겠습니다:
여기서 가장 큰 문제는 useQuery
에는 4개의 제네릭이 있다는 것입니다. 그 중 하나만 수동으로 제공하면 나머지 세 개는 기본값으로 돌아갑니다.
이것이 왜 나쁜지에 대해서는 #6: React Query and TypeScript 에서 확인할 수 있습니다.
같은 맥락에서 axios.get
은 any
를 반환하지만(fetch
와 마찬가지로), ky 는 기본적으로 unknown
을 반환하여 조금 더 낫습니다. 이 함수는 /todos/id
엔드포인트가 무엇을 반환할지 알지 못합니다. 그리고 data
속성이 any
가 되지 않기를 원하기 때문에 추론된 제네릭을 수동으로 제공하여 ‘재정의’해야 합니다. 아니면 우리가 할까요?
더 좋은 방법은 fetchTodo
함수 자체를 입력하는 것입니다:
이제 React Query는 queryFn
의 결과에서 어떤 데이터가 나올지 제대로 추론할 수 있습니다. 수동 제네릭이 필요 없습니다. useQuery
의 입력이 충분하면 괄호를 추가할 필요가 없습니다. 🎉
Lying angle brackets
또는 데이터 가져오기 레이어(이 경우 axios
)에 괄호를 통해 제네릭을 제공함으로써 예상되는 유형을 알려줄 수도 있습니다:
이제 타입 추론이 다시 작동하므로 원하지 않는 경우 fetchTodo
함수를 입력할 필요도 없습니다. 이러한 제네릭은 그 자체로 불필요한 것은 아니지만 제네릭의 황금률을 위반하기 때문에 거짓말입니다.
The golden rule of Generics
저는 이 규칙을 @danvdk’s 의 훌륭한 저서 Effective TypeScript 에서 배웠습니다. 이 책은 기본적으로 다음과 같이 말합니다:
제네릭이 유용하려면 최소 두 번 이상 표시되어야 합니다.
소위 “return-only” 제네릭은 변장된 타입 어설션에 지나지 않습니다. axios.get
의 (약간 단순화된) 타입 서명은 읽습니다:
타입 T는 return 타입 한 곳에만 나타납니다. 그래서 거짓말입니다! 그냥 as를 쓸 수도 있었어요:
적어도 이 타입 단언(as Todo
)은 숨겨져 있지 않고 명시적입니다. 이는 컴파일러를 우회하여 안전하지 않은 것을 가져와서 신뢰할 수 있는 것으로 바꾸려고 시도하고 있음을 보여줍니다.
Trust again
이제 우리는 다시 신뢰로 돌아갑니다. 우리가 유선을 통해 받는 정보가 실제로 특정 유형이라고 어떻게 믿을 수 있을까요? 우리는 그럴 수 없습니다.
저는 이 상황을 ‘신뢰할 수 있는 경계’라고 표현하곤 했습니다. 백엔드에서 반환되는 내용이 우리가 합의한 내용이라는 것을 믿어야 합니다. 그렇지 않다면 이는 우리의 잘못이 아니라 백엔드 팀의 잘못입니다.
물론 고객은 신경 쓰지 않습니다. 고객에게 표시되는 것은 “정의되지 않은 속성 이름을 읽을 수 없습니다” 또는 이와 유사한 메시지뿐입니다. 프론트엔드 개발자가 에스컬레이션에 호출되고, 오류가 완전히 다른 위치에 나타나기 때문에 실제로 유선을 통해 올바른 형태의 데이터가 전달되지 않는다는 것을 파악하는 데 상당한 시간이 걸립니다.
그렇다면 신뢰를 주기 위해 할 수 있는 일이 있을까요?
zod
zod 는 런타임에 유효성을 검사할 수 있는 스키마를 정의할 수 있는 멋진 유효성 검사 라이브러리입니다. 또한 스키마에서 직접 유효성 검사된 데이터의 타입을 추론합니다.
즉, 기본적으로 유형 정의를 작성한 다음 어떤 것이 해당 유형이라고 주장하는 대신 스키마를 작성하고 입력이 해당 스키마를 준수하는지 검증하면 그 시점에서 해당 유형이 됩니다.
저는 form 작업을 할 때 zod에 대해 처음 들었습니다. 사용자 입력의 유효성을 검사하는 것은 완전히 합리적입니다. 좋은 부수적인 효과로 유효성 검사 후에도 입력이 올바르게 입력됩니다. 하지만 사용자 입력의 유효성 검사뿐만 아니라 무엇이든 유효성 검사를 할 수 있습니다. 예를 들어 URL 매개변수. 또는 네트워크 응답…
validation in the queryFn
이전보다 코드가 더 늘어난 것도 아닙니다. 기본적으로 두 가지를 교환했습니다:
Todo
유형의 수동 유형 정의에todoSchema
정의를 추가합니다.- schema parsing으로 타입 단언합니다.
문제가 발생하면 parse
가 설명형 Error
를 던지고, 이 error
는 네트워크 호출 자체가 실패한 것처럼 React Query를 오류 상태로 만들기 때문에 React Query와 함께 매우 잘 작동합니다. 그리고 클라이언트 관점에서 보면, 예상한 구조를 반환하지 않았기 때문에 실패한 것입니다. 이제 우리는 어쨌든 처리해야 하는 error
상태를 갖게 되었으며 우리의 사용자가 놀랄 일은 없을 것입니다.
이는 제 또 다른 가이드라인과도 잘 어울립니다:
타입스크립트 코드가 자바스크립트처럼 보일수록 좋습니다.
id: number
외에는 이 TS 코드와 JS를 구분하는 요소는 하나도 없습니다. TypeScript의 복잡성이 추가되지 않고 type-safety의 이점만 누릴 수 있습니다. 타입 추론은 버터를 녹이는 뜨거운 칼처럼 코드를 통해 “흐릅니다”. 🤤
Tradeoffs
Schema parsing은 알아두면 좋은 개념이지만 무료는 아닙니다. 우선, 스키마는 원하는 만큼 탄력적이어야 합니다. 선택적 속성이 런타임에 null
이거나 undefined
인 경우가 중요하지 않다면, 그런 이유로 쿼리가 실패할 경우 사용자 환경이 엉망이 될 수 있습니다. 따라서 스키마를 탄력적으로 설계하세요.
또한 parsing은 런타임에 데이터를 분석하여 필요한 구조에 맞는지 확인해야 하므로 오버헤드가 발생합니다. 따라서 이 기술을 모든 곳에 적용하는 것은 적절하지 않을 수 있습니다.
What about getQueryData
queryClient.getQueryData
메서드도 같은 문제를 겪는다는 점을 알았을 것입니다. 이 메서드에는 반환 값에만 관련된 제네릭이 포함되어 있으며, 이를 제공하지 않으면 기본적으로 unknown
타입이 됩니다.
(미리 정의된 전체 스키마가 없기 때문에) React Query는 사용자가 QueryCache
에 무엇을 넣었는지 알 수 없으므로, 이것이 우리가 할 수 있는 최선입니다. 물론 스키마를 사용하여 getQueryData
의 결과를 파싱할 수도 있지만, 이전에 캐시된 데이터의 유효성을 검사한 적이 있다면 굳이 그럴 필요는 없습니다. 또한 QueryCache
와의 직접적인 상호 작용은 신중하게 수행해야 합니다.
react-query-kit 과 같은 React Query를 기반으로 하는 도구는 고통을 완화하는 데 큰 도움이 되지만 기본적으로 거짓말을 조금 더 숨길 수 있는 정도까지만 가능합니다.
Update
v5는 쿼리 옵션을 정의하는 새로운 방법을 제공하여
getQueryData
를 유형 안전하게 만들 수 있습니다. 자세한 내용은 문서 를 참조하세요.
End-to-end type-safety
이 점에서 React Query가 더 이상 할 수 있는 일은 많지 않지만, 다른 도구도 있습니다. 프론트엔드와 백엔드를 모두 제어하고 있고, 심지어 같은 모노레포에 함께 있다면 tRPC 나 zodios 와 같은 도구를 사용하는 것을 고려해 보세요. 이 두 가지 모두 클라이언트 측 데이터 불러오기 솔루션을 위해 React Query를 기반으로 구축되지만, 진정한 타입 안전성을 갖추기 위해 필요한 요소인 사전 API/라우터 정의가 있습니다.
이를 통해 백엔드에서 생성되는 모든 유형을 틀릴 가능성 없이 프런트엔드에서 유추할 수 있습니다. 또한 둘 다 스키마 정의에 zod
를 사용하므로(tRPC는 유효성 검사 라이브러리와 무관하지만 zod
가 가장 많이 사용됨), 2023년에 배울 목록에 zod
로 작업하는 방법을 추가하는 것이 좋습니다. 🎊