Node 라이브러리 중 Express를 사용해서 HTTP 요청을 손쉽게 사용할 수 있습니다.
URL 주소창 위에 https://localhost:3000/products/1
서버에게 요청 값이 1을 보냅니다.
주소창 위에 요청 파라미터를 Express는 어떻게 이 숫자를 요청 값을 인식할 수 있을까요?
Express 서버가 실행되는 코드는 다음과 같습니다.
import express from 'express'
const app = express();
app.get('/products/:n', (req, res) => {
res.send({
num: request.params.n
})
});
app.listen(3000, () => {
console.log('Server running...')
})
URL 주소에 products/1
을 보내면 Express 내부 코드에 get 메서드에서 req 파라미터에 1을 보내서 res 파라미터로 응답을 받습니다.
이 구조는 Express 요청/응답 단순한 코드로 구성이 되어 있습니다.
get
내부 구조에 대해서 자세히 살펴보겠습니다.
indes.d.ts
파일에 get은 Application Interface 파일에서 IRouterMatcher<this>
로 구현 되어 있습니다.
// interface
get: ((name: string) => any) & IRouterMatcher<this>;
IRouterMatcher<this>
는 this 즉 Application을 제네릭 파라미터로 사용하고 있습니다.
IRouterMatcher<this>
인터페이스는 내부 구조는 다음과 같이 되어 있습니다.
export interface IRouterMatcher<
T,
Method extends 'all' | 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head' = any
> {
<
Route extends string,
P = RouteParameters<Route>,
ResBody = any,
ReqBody = any,
ReqQuery = ParsedQs,
LocalsObj extends Record<string, any> = Record<string, any>
>(
// (it's used as the default type parameter for P)
// eslint-disable-next-line @definitelytyped/no-unnecessary-generics
path: Route,
// (This generic is meant to be passed explicitly.)
// eslint-disable-next-line @definitelytyped/no-unnecessary-generics
...handlers: Array<RequestHandler<P, ResBody, ReqBody, ReqQuery, LocalsObj>>
): T;
IRouterMatcher<T, Method>
로 구현이 되어 있습니다. app.get('/products/:n', (req, res)
의 app.get을 사용 할 수 있는 이유 입니다.
이제 ('/products/:n', (req, res)
는 IRouterMatcher에 구현 된 path:Route 와 ...handlers에 해당이 되는 것입니다.
export type RouteParameters<Route extends string> = string extends Route
? ParamsDictionary
: Route extends `${string}(${string}`
? ParamsDictionary //TODO: handling for regex parameters
: Route extends `${string}:${infer Rest}`
? (
GetRouteParameter<Rest> extends never
? ParamsDictionary
: GetRouteParameter<Rest> extends `${infer ParamName}?`
? { [P in ParamName]?: string }
: { [P in GetRouteParameter<Rest>]: string }
) &
(Rest extends `${GetRouteParameter<Rest>}${infer Next}`
? RouteParameters<Next> : unknown)
: {};
RouteParameters 타입 정의:
Route
라는 제네릭 타입을 받아들입니다. 이 타입은 문자열로 제한됩니다.타입 조건문:
첫 번째 조건문: string extends Route
- 만약 Route
가 문자열의 확장이라면, ParamsDictionary
타입을 반환합니다. 이는 기본적인 파라미터가 없는 경우를 다룹니다.
두 번째 조건문: Route가 ${string}(${string}
형태로 시작한다면, 아직 처리되지 않은 정규 표현식 파라미터를 다루기 위해 ParamsDictionary를 반환합니다. TODO 주석이 달려있어서 아직 구현되지 않은 상태입니다.
세 번째 조건문: Route
가 ${string}:${infer Rest}
형태로 시작한다면, 나머지 경로의 파라미터를 처리합니다.
파라미터 추출:
GetRouteParameter<Rest>
를 사용하여 나머지 경로에서 파라미터를 추출합니다. 이때 GetRouteParameter는 나머지 경로에서 다음 파라미터를 추출하는 역할을 합니다.
추출한 파라미터에 따라 다양한 경우를 다루는데,
GetRouteParameter<Rest> extends never
: 파라미터가 없는 경우에는 ParamsDictionary
를 반환합니다.
GetRouteParameter<Rest> extends ${infer ParamName}?
: 파라미터가 옵션인 경우에는 해당 파라미터를 포함한 객체를 반환합니다.
그 외의 경우에는 해당 파라미터를 필수로 간주하고, 해당 파라미터를 포함한 객체를 반환합니다.
재귀적 호출:
- 나머지 경로가 남아있는 경우, 재귀적으로 RouteParameters<Next>
를 호출하여 다음 경로의 파라미터를 처리합니다.
이 코드는 TypeScript의 많은 기능을 사용하여 유연하고 복잡한 라우팅 시스템에서 경로 파라미터를 추출하는 데 사용될 수 있습니다.
infer
는 TypeScript에서 사용되는 키워드로, 주로 제네릭 타입에서 타입 매개변수의 일부를 추출하는 데 사용됩니다.
여기서 infer Rest
는 타입 추론(inference
)을 통해 나머지 부분을 추출하라는 의미입니다. 이것은 주로 템플릿 리터럴 타입을 다룰 때 유용하게 사용됩니다.
예를 들어, ${string}:${infer Rest}
에서 ${string}
부분은 문자열이고, Rest는 나머지 부분을 나타냅니다. 그리고 이 Rest는 다시 다음 경로에 대한 정보를 담고 있는 것으로 추론됩니다.
type ExtractRest<T extends string> = T extends `${string}:${infer Rest}` ? Rest : never;
const example: ExtractRest<"users/:userId/posts/:postId">;
// 위의 예제에서는 "userId/posts/:postId"가 추출됨