Middleware
What is Middleware?
Middleware는 createServerFn
으로 생성된 서버 함수의 동작을 사용자 정의할 수 있도록 합니다. 이를 통해 공통된 검증, 컨텍스트 제공, 그리고 다양한 작업을 수행할 수 있습니다. Middleware는 계층적으로 실행되는 작업 체인을 생성하기 위해 다른 Middleware에 의존할 수도 있습니다.
What kinds of things can I do with Middleware?
- Authentication: 서버 함수 실행 전에 사용자의 신원을 확인합니다.
- Authorization: 사용자가 서버 함수를 실행할 권한이 있는지 확인합니다.
- Logging: 요청, 응답 및 에러를 로깅합니다.
- Observability: 메트릭, 트레이스 및 로그를 수집합니다.
- Provide Context: 다른 Middleware나 서버 함수에서 사용할 데이터를 요청 객체에 첨부합니다.
- Error Handling: 에러를 일관된 방식으로 처리합니다.
- 그리고 더 많은 작업이 가능합니다! 가능성은 여러분의 상상에 달려 있습니다.
Defining Middleware
Middleware는 createMiddleware
함수를 사용하여 정의됩니다. 이 함수는 middleware
, validator
, server
, client
와 같은 메서드로 Middleware를 계속 사용자 정의할 수 있는 Middleware
객체를 반환합니다.
import { createMiddleware } from "@tanstack/start";
const loggingMiddleware = createMiddleware().server(async ({ next, data }) => {
console.log("Request received:", data);
const result = await next();
console.log("Response processed:", result);
return result;
});
Middleware Methods
Middleware를 사용자 정의하기 위해 여러 메서드를 사용할 수 있습니다. TypeScript를 사용하는 경우, 이러한 메서드의 순서는 타입 시스템에 의해 강제되어 최대한의 추론 및 타입 안전성을 보장합니다.
middleware
: 체인에 Middleware를 추가합니다.validator
: 데이터를 수정하여 이 Middleware 및 하위 Middleware로 전달합니다.server
: 하위 Middleware 및 서버 함수 실행 전에 서버 측 논리를 정의하고, 결과를 다음 Middleware로 전달합니다.client
: 하위 Middleware 및 최종 RPC 함수 실행 전에 클라이언트 측 논리를 정의하고, 결과를 다음 Middleware로 전달합니다.
The middleware
method
middleware
메서드는 현재 Middleware 이전에 실행될 Middleware를 체인에 추가하는 데 사용됩니다. middleware
메서드에 Middleware 객체 배열을 전달합니다.
const loggingMiddleware = createMiddleware().middleware([
authMiddleware,
loggingMiddleware,
]);
상위 Middleware에서 타입 안전한 컨텍스트와 페이로드 검증도 상속됩니다!
The validator
method
validator
메서드는 데이터를 수정하여 이 Middleware, 하위 Middleware, 그리고 최종적으로 서버 함수로 전달합니다. 이 메서드는 데이터 객체를 받아 검증된(그리고 선택적으로 수정된) 데이터 객체를 반환하는 함수를 받아야 합니다. 일반적으로 zod
와 같은 검증 라이브러리를 사용합니다. 예제는 다음과 같습니다:
import { z } from "zod";
const mySchema = z.object({
workspaceId: z.string(),
});
const workspaceMiddleware = createMiddleware()
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log("Workspace ID:", data.workspaceId);
return next();
});
The server
method
server
메서드는 Middleware가 하위 Middleware 및 서버 함수 실행 전후에 실행할 서버 측 논리를 정의하는 데 사용됩니다. 이 메서드는 다음 속성을 가진 객체를 받습니다:
next
: 호출 시 체인의 다음 Middleware를 실행하는 함수입니다.data
: 서버 함수에 전달된 데이터 객체입니다.context
: 상위 Middleware에서 전달된 데이터를 저장하는 객체입니다. 추가 데이터를 확장하여 하위 Middleware로 전달할 수 있습니다.
Returning the required result from next
next
함수는 체인의 다음 Middleware를 실행하는 데 사용됩니다. 체인의 실행을 계속하려면 제공된 next
함수의 결과를 반드시 대기(await)하고 반환(return)해야 합니다.
const loggingMiddleware = createMiddleware().server(async ({ next }) => {
console.log("Request received");
const result = await next();
console.log("Response processed");
return result;
});
Providing context to the next middleware via next
next
함수는 선택적으로 context
속성을 가진 객체를 호출할 수 있습니다. 이 context
값에 전달된 속성은 상위 context
에 병합되어 다음 Middleware에 제공됩니다.
const awesomeMiddleware = createMiddleware().server(({ next }) => {
return next({
context: {
isAwesome: Math.random() > 0.5,
},
});
});
const loggingMiddleware = createMiddleware().server(
async ({ next, context }) => {
console.log("Is awesome?", context.isAwesome);
return next();
}
);
Client-Side Logic
서버 함수가 주로 서버 측 작업이지만, 클라이언트에서 RPC 요청을 둘러싼 많은 클라이언트 측 논리가 있습니다. 이는 클라이언트 측 Middleware에서 논리를 정의하여 하위 Middleware와 최종 RPC 함수 및 클라이언트 응답을 실행할 수 있다는 것을 의미합니다.
Client-side Payload Validation
기본적으로 Middleware 검증은 클라이언트 번들의 크기를 작게 유지하기 위해 서버에서만 수행됩니다. 그러나 createMiddleware
함수에 validateClient: true
옵션을 전달하여 클라이언트에서 데이터를 검증하도록 선택할 수 있습니다. 이렇게 하면 데이터를 서버로 전송하기 전에 검증하여 네트워크 왕복을 절약할 수 있습니다.
왜 클라이언트에서 다른 검증 스키마를 전달할 수 없나요?
클라이언트 검증 스키마는 서버 검증 스키마에서 파생됩니다. 이는 클라이언트 검증 스키마가 데이터를 서버로 보내기 전에 이를 검증하는 데 사용되기 때문입니다. 클라이언트 검증 스키마가 서버 검증 스키마와 다르면 서버가 예상하지 못한 데이터를 수신할 수 있으며, 이는 예기치 않은 동작을 초래할 수 있습니다.
const workspaceMiddleware = createMiddleware({ validateClient: true })
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log("Workspace ID:", data.workspaceId);
return next();
});
The client
method
클라이언트 Middleware 논리는 Middleware
객체에서 client
메서드를 사용하여 정의됩니다. 이 메서드는 하위 Middleware 및 최종 RPC 함수(또는 서버 함수)가 실행되기 전후에 클라이언트 측 논리를 정의하는 데 사용됩니다.
클라이언트 Middleware 논리는 server
메서드로 생성된 논리와 유사한 API를 공유하지만, 클라이언트 측에서 실행됩니다.
- 체인을 계속하기 위해
next
함수가 호출되어야 합니다. next
함수를 통해 다음 클라이언트 Middleware에 컨텍스트를 제공할 수 있습니다.- 데이터를 수정하여 다음 클라이언트 Middleware로 전달할 수 있습니다.
const loggingMiddleware = createMiddleware().client(async ({ next }) => {
console.log("Request sent");
const result = await next();
console.log("Response received");
return result;
});
Sending client context to the server
클라이언트 컨텍스트는 기본적으로 서버로 전송되지 않습니다. 클라이언트에서 서버로 컨텍스트를 보내려면 next
함수에 sendContext
속성을 전달해야 합니다.
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
workspaceId: context.workspaceId,
},
});
})
.server(async ({ next, data, context }) => {
console.log("Workspace ID:", context.workspaceId);
return next();
});
Client-Sent Context Security
클라이언트가 서버로 보낸 데이터는 런타임에 검증되지 않을 수 있습니다. 동적 데이터를 전송할 경우 서버 Middleware에서 이를 검증하세요:
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
workspaceId: context.workspaceId,
},
});
})
.server(async ({ next, data, context }) => {
const workspaceId = zodValidator(z.number()).parse(context.workspaceId);
console.log("Workspace ID:", workspaceId);
return next();
});
Sending server context to the client
서버 컨텍스트를 클라이언트로 보내려면 next
함수에 sendContext
속성을 전달합니다.
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
const result = next();
console.log("Time from the server:", result.context.timeFromServer);
})
.server(async ({ next }) => {
return next({
sendContext: {
timeFromServer: new Date(),
},
});
});
Middleware Execution Order
Middleware는 의존성 우선 순서로 실행됩니다. 아래 예제는 a
, b
, c
, d
순으로 실행됩니다:
const a = createMiddleware().server(async ({ next }) => {
console.log("a");
return next();
});
const b = createMiddleware()
.middleware([a])
.server(async ({ next }) => {
console.log("b");
return next();
});
const c = createMiddleware()
.middleware()
.server(async ({ next }) => {
console.log("c");
return next();
});
const d = createMiddleware()
.middleware([b, c])
.server(async () => {
console.log("d");
});
const fn = createServerFn()
.middleware([d])
.server(async () => {
console.log("fn");
});
Environment Tree Shaking
Middleware 기능은 생성된 각 번들의 환경에 따라 tree-shaken 됩니다.
- 서버에서는 tree-shaking이 발생하지 않으므로 Middleware에서 사용된 모든 코드가 서버 번들에 포함됩니다.
- 클라이언트에서는 모든 서버 전용 코드가 클라이언트 번들에서 제거됩니다. 이는
server
메서드에서 사용된 모든 코드가 항상 클라이언트 번들에서 제거된다는 것을 의미합니다. 만약validateClient
가true
로 설정되어 있으면 클라이언트 측 검증 코드는 클라이언트 번들에 포함됩니다. 그렇지 않으면data
검증 코드도 제거됩니다.