@mandujs/core 0.5.7 β†’ 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Mandu Middleware Compose πŸ”—
3
+ * Hono μŠ€νƒ€μΌ 미듀웨어 μ‘°ν•© νŒ¨ν„΄
4
+ *
5
+ * @see https://github.com/honojs/hono/blob/main/src/compose.ts
6
+ */
7
+
8
+ import type { ManduContext } from "../filling/context";
9
+
10
+ /**
11
+ * Next ν•¨μˆ˜ νƒ€μž…
12
+ */
13
+ export type Next = () => Promise<void>;
14
+
15
+ /**
16
+ * 미듀웨어 ν•¨μˆ˜ νƒ€μž…
17
+ * - Response λ°˜ν™˜: 체인 쀑단 (Guard μ—­ν• )
18
+ * - void λ°˜ν™˜: λ‹€μŒ 미듀웨어 μ‹€ν–‰
19
+ */
20
+ export type Middleware = (
21
+ ctx: ManduContext,
22
+ next: Next
23
+ ) => Response | void | Promise<Response | void>;
24
+
25
+ /**
26
+ * μ—λŸ¬ ν•Έλ“€λŸ¬ νƒ€μž…
27
+ */
28
+ export type ErrorHandler = (
29
+ error: Error,
30
+ ctx: ManduContext
31
+ ) => Response | Promise<Response>;
32
+
33
+ /**
34
+ * NotFound ν•Έλ“€λŸ¬ νƒ€μž…
35
+ */
36
+ export type NotFoundHandler = (ctx: ManduContext) => Response | Promise<Response>;
37
+
38
+ /**
39
+ * 미듀웨어 μ—”νŠΈλ¦¬ (메타데이터 포함)
40
+ */
41
+ export interface MiddlewareEntry {
42
+ fn: Middleware;
43
+ name?: string;
44
+ isAsync?: boolean;
45
+ }
46
+
47
+ /**
48
+ * Compose μ˜΅μ…˜
49
+ */
50
+ export interface ComposeOptions {
51
+ onError?: ErrorHandler;
52
+ onNotFound?: NotFoundHandler;
53
+ }
54
+
55
+ /**
56
+ * 미듀웨어 ν•¨μˆ˜λ“€μ„ ν•˜λ‚˜μ˜ μ‹€ν–‰ ν•¨μˆ˜λ‘œ μ‘°ν•©
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const middleware = [
61
+ * { fn: async (ctx, next) => { console.log('before'); await next(); console.log('after'); } },
62
+ * { fn: async (ctx, next) => { return ctx.ok({ data: 'hello' }); } },
63
+ * ];
64
+ *
65
+ * const handler = compose(middleware, {
66
+ * onError: (err, ctx) => ctx.json({ error: err.message }, 500),
67
+ * onNotFound: (ctx) => ctx.notFound(),
68
+ * });
69
+ *
70
+ * const response = await handler(context);
71
+ * ```
72
+ */
73
+ export function compose(
74
+ middleware: MiddlewareEntry[],
75
+ options: ComposeOptions = {}
76
+ ): (ctx: ManduContext) => Promise<Response> {
77
+ const { onError, onNotFound } = options;
78
+
79
+ return async (ctx: ManduContext): Promise<Response> => {
80
+ let index = -1;
81
+ let finalResponse: Response | undefined;
82
+
83
+ /**
84
+ * 미듀웨어 순차 μ‹€ν–‰
85
+ * @param i ν˜„μž¬ 인덱슀
86
+ */
87
+ async function dispatch(i: number): Promise<void> {
88
+ // next() 이쀑 호좜 λ°©μ§€
89
+ if (i <= index) {
90
+ throw new Error("next() called multiple times");
91
+ }
92
+ index = i;
93
+
94
+ const entry = middleware[i];
95
+
96
+ if (!entry) {
97
+ // λͺ¨λ“  미듀웨어 톡과 ν›„ ν•Έλ“€λŸ¬ μ—†μŒ
98
+ if (!finalResponse && onNotFound) {
99
+ finalResponse = await onNotFound(ctx);
100
+ }
101
+ return;
102
+ }
103
+
104
+ try {
105
+ const result = await entry.fn(ctx, () => dispatch(i + 1));
106
+
107
+ // Response λ°˜ν™˜ μ‹œ 체인 쀑단
108
+ if (result instanceof Response) {
109
+ finalResponse = result;
110
+ return;
111
+ }
112
+ } catch (err) {
113
+ if (err instanceof Error && onError) {
114
+ finalResponse = await onError(err, ctx);
115
+ return;
116
+ }
117
+ throw err;
118
+ }
119
+ }
120
+
121
+ await dispatch(0);
122
+
123
+ // 응닡이 μ—†μœΌλ©΄ 404
124
+ if (!finalResponse) {
125
+ if (onNotFound) {
126
+ finalResponse = await onNotFound(ctx);
127
+ } else {
128
+ finalResponse = new Response("Not Found", { status: 404 });
129
+ }
130
+ }
131
+
132
+ return finalResponse;
133
+ };
134
+ }
135
+
136
+ /**
137
+ * 미듀웨어 λ°°μ—΄ 생성 헬퍼
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const mw = createMiddleware([
142
+ * authGuard,
143
+ * rateLimitGuard,
144
+ * mainHandler,
145
+ * ]);
146
+ * ```
147
+ */
148
+ export function createMiddleware(
149
+ fns: Middleware[]
150
+ ): MiddlewareEntry[] {
151
+ return fns.map((fn, i) => ({
152
+ fn,
153
+ name: fn.name || `middleware_${i}`,
154
+ isAsync: fn.constructor.name === "AsyncFunction",
155
+ }));
156
+ }
157
+
158
+ /**
159
+ * 미듀웨어 체인 λΉŒλ”
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const chain = new MiddlewareChain()
164
+ * .use(authGuard)
165
+ * .use(rateLimitGuard)
166
+ * .use(mainHandler)
167
+ * .onError((err, ctx) => ctx.json({ error: err.message }, 500))
168
+ * .build();
169
+ *
170
+ * const response = await chain(ctx);
171
+ * ```
172
+ */
173
+ export class MiddlewareChain {
174
+ private middleware: MiddlewareEntry[] = [];
175
+ private errorHandler?: ErrorHandler;
176
+ private notFoundHandler?: NotFoundHandler;
177
+
178
+ /**
179
+ * 미듀웨어 μΆ”κ°€
180
+ */
181
+ use(fn: Middleware, name?: string): this {
182
+ this.middleware.push({
183
+ fn,
184
+ name: name || fn.name || `middleware_${this.middleware.length}`,
185
+ isAsync: fn.constructor.name === "AsyncFunction",
186
+ });
187
+ return this;
188
+ }
189
+
190
+ /**
191
+ * μ—λŸ¬ ν•Έλ“€λŸ¬ μ„€μ •
192
+ */
193
+ onError(handler: ErrorHandler): this {
194
+ this.errorHandler = handler;
195
+ return this;
196
+ }
197
+
198
+ /**
199
+ * NotFound ν•Έλ“€λŸ¬ μ„€μ •
200
+ */
201
+ onNotFound(handler: NotFoundHandler): this {
202
+ this.notFoundHandler = handler;
203
+ return this;
204
+ }
205
+
206
+ /**
207
+ * 미듀웨어 체인 λΉŒλ“œ
208
+ */
209
+ build(): (ctx: ManduContext) => Promise<Response> {
210
+ return compose(this.middleware, {
211
+ onError: this.errorHandler,
212
+ onNotFound: this.notFoundHandler,
213
+ });
214
+ }
215
+
216
+ /**
217
+ * 미듀웨어 λͺ©λ‘ 쑰회
218
+ */
219
+ getMiddleware(): MiddlewareEntry[] {
220
+ return [...this.middleware];
221
+ }
222
+ }
@@ -3,3 +3,5 @@ export * from "./router";
3
3
  export * from "./server";
4
4
  export * from "./cors";
5
5
  export * from "./env";
6
+ export * from "./compose";
7
+ export * from "./lifecycle";
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Mandu Lifecycle Hooks πŸ”„
3
+ * Elysia μŠ€νƒ€μΌ 라이프사이클 ν›… 체계
4
+ *
5
+ * @see https://elysiajs.com/life-cycle/overview.html
6
+ *
7
+ * μš”μ²­ 흐름:
8
+ * 1. onRequest - μš”μ²­ μ‹œμž‘
9
+ * 2. onParse - λ°”λ”” νŒŒμ‹± (POST, PUT, PATCH)
10
+ * 3. beforeHandle - ν•Έλ“€λŸ¬ μ „ (Guard μ—­ν• )
11
+ * 4. [Handler] - 메인 ν•Έλ“€λŸ¬ μ‹€ν–‰
12
+ * 5. afterHandle - ν•Έλ“€λŸ¬ ν›„ (응닡 λ³€ν™˜)
13
+ * 6. mapResponse - 응닡 λ§€ν•‘
14
+ * 7. afterResponse - 응닡 ν›„ (λ‘œκΉ…, 정리)
15
+ *
16
+ * μ—λŸ¬ λ°œμƒ μ‹œ:
17
+ * - onError - μ—λŸ¬ 핸듀링
18
+ */
19
+
20
+ import type { ManduContext } from "../filling/context";
21
+
22
+ /**
23
+ * ν›… μŠ€μ½”ν”„
24
+ * - global: λͺ¨λ“  λΌμš°νŠΈμ— 적용
25
+ * - scoped: ν˜„μž¬ ν”ŒλŸ¬κ·ΈμΈ/라우트 그룹에 적용
26
+ * - local: ν˜„μž¬ λΌμš°νŠΈμ—λ§Œ 적용
27
+ */
28
+ export type HookScope = "global" | "scoped" | "local";
29
+
30
+ /**
31
+ * ν›… μ»¨ν…Œμ΄λ„ˆ
32
+ */
33
+ export interface HookContainer<T extends Function = Function> {
34
+ fn: T;
35
+ scope: HookScope;
36
+ name?: string;
37
+ checksum?: number; // 쀑볡 제거용
38
+ }
39
+
40
+ // ============================================
41
+ // ν›… νƒ€μž… μ •μ˜
42
+ // ============================================
43
+
44
+ /** μš”μ²­ μ‹œμž‘ ν›… */
45
+ export type OnRequestHandler = (ctx: ManduContext) => void | Promise<void>;
46
+
47
+ /** λ°”λ”” νŒŒμ‹± ν›… */
48
+ export type OnParseHandler = (ctx: ManduContext) => void | Promise<void>;
49
+
50
+ /** ν•Έλ“€λŸ¬ μ „ ν›… (Guard μ—­ν• ) - Response λ°˜ν™˜ μ‹œ 체인 쀑단 */
51
+ export type BeforeHandleHandler = (
52
+ ctx: ManduContext
53
+ ) => Response | void | Promise<Response | void>;
54
+
55
+ /** ν•Έλ“€λŸ¬ ν›„ ν›… - 응닡 λ³€ν™˜ κ°€λŠ₯ */
56
+ export type AfterHandleHandler = (
57
+ ctx: ManduContext,
58
+ response: Response
59
+ ) => Response | Promise<Response>;
60
+
61
+ /** 응닡 λ§€ν•‘ ν›… */
62
+ export type MapResponseHandler = (
63
+ ctx: ManduContext,
64
+ response: Response
65
+ ) => Response | Promise<Response>;
66
+
67
+ /** 응닡 ν›„ ν›… (비동기, 응닡에 영ν–₯ μ—†μŒ) */
68
+ export type AfterResponseHandler = (ctx: ManduContext) => void | Promise<void>;
69
+
70
+ /** μ—λŸ¬ 핸듀링 ν›… - Response λ°˜ν™˜ μ‹œ μ—λŸ¬ μ‘λ‹΅μœΌλ‘œ μ‚¬μš© */
71
+ export type OnErrorHandler = (
72
+ ctx: ManduContext,
73
+ error: Error
74
+ ) => Response | void | Promise<Response | void>;
75
+
76
+ // ============================================
77
+ // 라이프사이클 μŠ€ν† μ–΄
78
+ // ============================================
79
+
80
+ /**
81
+ * 라이프사이클 ν›… μŠ€ν† μ–΄
82
+ */
83
+ export interface LifecycleStore {
84
+ onRequest: HookContainer<OnRequestHandler>[];
85
+ onParse: HookContainer<OnParseHandler>[];
86
+ beforeHandle: HookContainer<BeforeHandleHandler>[];
87
+ afterHandle: HookContainer<AfterHandleHandler>[];
88
+ mapResponse: HookContainer<MapResponseHandler>[];
89
+ afterResponse: HookContainer<AfterResponseHandler>[];
90
+ onError: HookContainer<OnErrorHandler>[];
91
+ }
92
+
93
+ /**
94
+ * 빈 라이프사이클 μŠ€ν† μ–΄ 생성
95
+ */
96
+ export function createLifecycleStore(): LifecycleStore {
97
+ return {
98
+ onRequest: [],
99
+ onParse: [],
100
+ beforeHandle: [],
101
+ afterHandle: [],
102
+ mapResponse: [],
103
+ afterResponse: [],
104
+ onError: [],
105
+ };
106
+ }
107
+
108
+ // ============================================
109
+ // 라이프사이클 μ‹€ν–‰
110
+ // ============================================
111
+
112
+ /**
113
+ * 라이프사이클 μ‹€ν–‰ μ˜΅μ…˜
114
+ */
115
+ export interface ExecuteOptions {
116
+ /** λ°”λ”” νŒŒμ‹±μ΄ ν•„μš”ν•œ λ©”μ„œλ“œ */
117
+ parseBodyMethods?: string[];
118
+ }
119
+
120
+ const DEFAULT_PARSE_BODY_METHODS = ["POST", "PUT", "PATCH"];
121
+
122
+ /**
123
+ * 라이프사이클 μ‹€ν–‰
124
+ *
125
+ * @param lifecycle 라이프사이클 μŠ€ν† μ–΄
126
+ * @param ctx ManduContext
127
+ * @param handler 메인 ν•Έλ“€λŸ¬
128
+ * @param options μ˜΅μ…˜
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const lifecycle = createLifecycleStore();
133
+ * lifecycle.onRequest.push({ fn: (ctx) => console.log('Request started'), scope: 'local' });
134
+ * lifecycle.beforeHandle.push({ fn: authGuard, scope: 'local' });
135
+ *
136
+ * const response = await executeLifecycle(
137
+ * lifecycle,
138
+ * ctx,
139
+ * async () => ctx.ok({ data: 'hello' })
140
+ * );
141
+ * ```
142
+ */
143
+ export async function executeLifecycle(
144
+ lifecycle: LifecycleStore,
145
+ ctx: ManduContext,
146
+ handler: () => Promise<Response>,
147
+ options: ExecuteOptions = {}
148
+ ): Promise<Response> {
149
+ const { parseBodyMethods = DEFAULT_PARSE_BODY_METHODS } = options;
150
+ let response: Response;
151
+
152
+ try {
153
+ // 1. onRequest
154
+ for (const hook of lifecycle.onRequest) {
155
+ await hook.fn(ctx);
156
+ }
157
+
158
+ // 2. onParse (λ°”λ””κ°€ μžˆλŠ” λ©”μ„œλ“œλ§Œ)
159
+ if (parseBodyMethods.includes(ctx.req.method)) {
160
+ for (const hook of lifecycle.onParse) {
161
+ await hook.fn(ctx);
162
+ }
163
+ }
164
+
165
+ // 3. beforeHandle (Guard μ—­ν• )
166
+ for (const hook of lifecycle.beforeHandle) {
167
+ const result = await hook.fn(ctx);
168
+ if (result instanceof Response) {
169
+ // Response λ°˜ν™˜ μ‹œ 체인 쀑단, afterHandle/mapResponse κ±΄λ„ˆλœ€
170
+ response = result;
171
+ // afterResponseλŠ” μ‹€ν–‰
172
+ scheduleAfterResponse(lifecycle.afterResponse, ctx);
173
+ return response;
174
+ }
175
+ }
176
+
177
+ // 4. 메인 ν•Έλ“€λŸ¬ μ‹€ν–‰
178
+ response = await handler();
179
+
180
+ // 5. afterHandle
181
+ for (const hook of lifecycle.afterHandle) {
182
+ response = await hook.fn(ctx, response);
183
+ }
184
+
185
+ // 6. mapResponse
186
+ for (const hook of lifecycle.mapResponse) {
187
+ response = await hook.fn(ctx, response);
188
+ }
189
+
190
+ // 7. afterResponse (비동기)
191
+ scheduleAfterResponse(lifecycle.afterResponse, ctx);
192
+
193
+ return response;
194
+ } catch (err) {
195
+ // onError 처리
196
+ const error = err instanceof Error ? err : new Error(String(err));
197
+
198
+ for (const hook of lifecycle.onError) {
199
+ const result = await hook.fn(ctx, error);
200
+ if (result instanceof Response) {
201
+ // afterResponseλŠ” μ—λŸ¬ μ‹œμ—λ„ μ‹€ν–‰
202
+ scheduleAfterResponse(lifecycle.afterResponse, ctx);
203
+ return result;
204
+ }
205
+ }
206
+
207
+ // μ—λŸ¬ ν•Έλ“€λŸ¬κ°€ Responseλ₯Ό λ°˜ν™˜ν•˜μ§€ μ•ŠμœΌλ©΄ 재throw
208
+ throw error;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * afterResponse ν›… 비동기 μ‹€ν–‰ (응닡 ν›„)
214
+ */
215
+ function scheduleAfterResponse(
216
+ hooks: HookContainer<AfterResponseHandler>[],
217
+ ctx: ManduContext
218
+ ): void {
219
+ if (hooks.length === 0) return;
220
+
221
+ // queueMicrotask둜 응닡 ν›„ μ‹€ν–‰
222
+ queueMicrotask(async () => {
223
+ for (const hook of hooks) {
224
+ try {
225
+ await hook.fn(ctx);
226
+ } catch (err) {
227
+ console.error("[Mandu] afterResponse hook error:", err);
228
+ }
229
+ }
230
+ });
231
+ }
232
+
233
+ // ============================================
234
+ // 라이프사이클 λΉŒλ”
235
+ // ============================================
236
+
237
+ /**
238
+ * 라이프사이클 λΉŒλ”
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * const lifecycle = new LifecycleBuilder()
243
+ * .onRequest((ctx) => console.log('Request:', ctx.req.url))
244
+ * .beforeHandle(authGuard)
245
+ * .afterHandle((ctx, res) => {
246
+ * // 응닡 헀더 μΆ”κ°€
247
+ * res.headers.set('X-Custom', 'value');
248
+ * return res;
249
+ * })
250
+ * .onError((ctx, err) => ctx.json({ error: err.message }, 500))
251
+ * .build();
252
+ * ```
253
+ */
254
+ export class LifecycleBuilder {
255
+ private store: LifecycleStore = createLifecycleStore();
256
+
257
+ /**
258
+ * μš”μ²­ μ‹œμž‘ ν›… μΆ”κ°€
259
+ */
260
+ onRequest(fn: OnRequestHandler, scope: HookScope = "local"): this {
261
+ this.store.onRequest.push({ fn, scope });
262
+ return this;
263
+ }
264
+
265
+ /**
266
+ * λ°”λ”” νŒŒμ‹± ν›… μΆ”κ°€
267
+ */
268
+ onParse(fn: OnParseHandler, scope: HookScope = "local"): this {
269
+ this.store.onParse.push({ fn, scope });
270
+ return this;
271
+ }
272
+
273
+ /**
274
+ * ν•Έλ“€λŸ¬ μ „ ν›… μΆ”κ°€ (Guard μ—­ν• )
275
+ */
276
+ beforeHandle(fn: BeforeHandleHandler, scope: HookScope = "local"): this {
277
+ this.store.beforeHandle.push({ fn, scope });
278
+ return this;
279
+ }
280
+
281
+ /**
282
+ * ν•Έλ“€λŸ¬ ν›„ ν›… μΆ”κ°€
283
+ */
284
+ afterHandle(fn: AfterHandleHandler, scope: HookScope = "local"): this {
285
+ this.store.afterHandle.push({ fn, scope });
286
+ return this;
287
+ }
288
+
289
+ /**
290
+ * 응닡 λ§€ν•‘ ν›… μΆ”κ°€
291
+ */
292
+ mapResponse(fn: MapResponseHandler, scope: HookScope = "local"): this {
293
+ this.store.mapResponse.push({ fn, scope });
294
+ return this;
295
+ }
296
+
297
+ /**
298
+ * 응닡 ν›„ ν›… μΆ”κ°€
299
+ */
300
+ afterResponse(fn: AfterResponseHandler, scope: HookScope = "local"): this {
301
+ this.store.afterResponse.push({ fn, scope });
302
+ return this;
303
+ }
304
+
305
+ /**
306
+ * μ—λŸ¬ 핸듀링 ν›… μΆ”κ°€
307
+ */
308
+ onError(fn: OnErrorHandler, scope: HookScope = "local"): this {
309
+ this.store.onError.push({ fn, scope });
310
+ return this;
311
+ }
312
+
313
+ /**
314
+ * 라이프사이클 μŠ€ν† μ–΄ λΉŒλ“œ
315
+ */
316
+ build(): LifecycleStore {
317
+ return { ...this.store };
318
+ }
319
+
320
+ /**
321
+ * λ‹€λ₯Έ 라이프사이클과 병합
322
+ */
323
+ merge(other: LifecycleStore): this {
324
+ this.store.onRequest.push(...other.onRequest);
325
+ this.store.onParse.push(...other.onParse);
326
+ this.store.beforeHandle.push(...other.beforeHandle);
327
+ this.store.afterHandle.push(...other.afterHandle);
328
+ this.store.mapResponse.push(...other.mapResponse);
329
+ this.store.afterResponse.push(...other.afterResponse);
330
+ this.store.onError.push(...other.onError);
331
+ return this;
332
+ }
333
+ }
334
+
335
+ // ============================================
336
+ // μœ ν‹Έλ¦¬ν‹°
337
+ // ============================================
338
+
339
+ /**
340
+ * ν›… 쀑볡 제거 (checksum 기반)
341
+ */
342
+ export function deduplicateHooks<T extends HookContainer>(hooks: T[]): T[] {
343
+ const seen = new Set<number>();
344
+ return hooks.filter((hook) => {
345
+ if (hook.checksum === undefined) return true;
346
+ if (seen.has(hook.checksum)) return false;
347
+ seen.add(hook.checksum);
348
+ return true;
349
+ });
350
+ }
351
+
352
+ /**
353
+ * μŠ€μ½”ν”„λ³„ ν›… 필터링
354
+ */
355
+ export function filterHooksByScope<T extends HookContainer>(
356
+ hooks: T[],
357
+ scopes: HookScope[]
358
+ ): T[] {
359
+ return hooks.filter((hook) => scopes.includes(hook.scope));
360
+ }
@@ -315,6 +315,9 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
315
315
  let loaderData: unknown;
316
316
  let component: RouteComponent | undefined;
317
317
 
318
+ // Client-side Routing: 데이터 μš”μ²­ 감지
319
+ const isDataRequest = url.searchParams.has("_data");
320
+
318
321
  // 1. PageHandler 방식 (μ‹ κ·œ - filling 포함)
319
322
  const pageHandler = pageHandlers.get(route.id);
320
323
  if (pageHandler) {
@@ -363,6 +366,18 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
363
366
  }
364
367
  }
365
368
 
369
+ // Client-side Routing: λ°μ΄ν„°λ§Œ λ°˜ν™˜ (JSON)
370
+ if (isDataRequest) {
371
+ return Response.json({
372
+ routeId: route.id,
373
+ pattern: route.pattern,
374
+ params,
375
+ loaderData: loaderData ?? null,
376
+ timestamp: Date.now(),
377
+ });
378
+ }
379
+
380
+ // SSR λ Œλ”λ§ (κΈ°μ‘΄ 둜직)
366
381
  const appCreator = createAppFn || defaultCreateApp;
367
382
  try {
368
383
  const app = appCreator({
@@ -385,6 +400,9 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
385
400
  hydration: route.hydration,
386
401
  bundleManifest: serverSettings.bundleManifest,
387
402
  serverData,
403
+ // Client-side Routing ν™œμ„±ν™” 정보 전달
404
+ enableClientRouter: true,
405
+ routePattern: route.pattern,
388
406
  });
389
407
  } catch (err) {
390
408
  const ssrError = createSSRErrorResponse(