๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu๋Š” Fastify๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋Š” ๊ณ ์„ฑ๋Šฅ HTTP ์„œ๋ฒ„์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„ ํฌํŠธ, ํ˜ธ์ŠคํŠธ, ํ”Œ๋Ÿฌ๊ทธ์ธ, ๋ผ์ดํ”„์‚ฌ์ดํด ํ›… ๋“ฑ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ๊ตฌ์กฐ

import path from "path";
import { defineConfig } from "sonamu";

const host = "localhost";
const port = 1028;

export default defineConfig({
  server: {
    baseUrl: `http://${host}:${port}`,
    listen: { port, host },
    
    plugins: {
      formbody: true,
      qs: true,
      multipart: { limits: { fileSize: 1024 * 1024 * 30 } },
      static: {
        root: path.join(import.meta.dirname, "/../", "public"),
        prefix: "/api/public",
      },
      session: {
        secret: process.env.SESSION_SECRET,
        salt: process.env.SESSION_SALT,
      },
    },
    
    apiConfig: {
      contextProvider: (defaultContext, request) => ({
        ...defaultContext,
        ip: request.ip,
      }),
    },
    
    lifecycle: {
      onStart: () => console.log(`๐ŸŒฒ Server listening on http://${host}:${port}`),
      onShutdown: () => console.log("graceful shutdown"),
    },
  },
  // ...
});

baseUrl

์™ธ๋ถ€์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ์„œ๋ฒ„์˜ ์ „์ฒด URL์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: string (์„ ํƒ์ ) ๊ธฐ๋ณธ๊ฐ’: http://{listen.host}:{listen.port}
const host = "localhost";
const port = 1028;

export default defineConfig({
  server: {
    baseUrl: `http://${host}:${port}`,
    // ...
  },
});
์‹ค์ œ ๋„๋ฉ”์ธ ์‚ฌ์šฉ:
export default defineConfig({
  server: {
    baseUrl: "https://api.myapp.com",  // ํ”„๋กœ๋•์…˜ URL
    listen: { port: 3000, host: "0.0.0.0" },
  },
});
baseUrl์€ ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ ์ƒ์„ฑ๋˜๋Š” URL, SSR ๋ฉ”ํƒ€ ํƒœ๊ทธ ๋“ฑ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

listen

์„œ๋ฒ„๊ฐ€ ์ˆ˜์‹ ํ•  ํฌํŠธ์™€ ํ˜ธ์ŠคํŠธ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: (์„ ํƒ์ )
listen?: {
  port: number;
  host?: string;
}

port

์„œ๋ฒ„๊ฐ€ ์ˆ˜์‹ ํ•  ํฌํŠธ ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค. ํƒ€์ž…: number
export default defineConfig({
  server: {
    listen: {
      port: 1028,  // ์ด ํฌํŠธ์—์„œ ์ˆ˜์‹ 
    },
  },
});
ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ํฌํŠธ ์„ค์ •:
const port = Number(process.env.PORT ?? 1028);

export default defineConfig({
  server: {
    listen: { port },
  },
});

host

์„œ๋ฒ„๊ฐ€ ๋ฐ”์ธ๋”ฉํ•  ํ˜ธ์ŠคํŠธ ์ฃผ์†Œ์ž…๋‹ˆ๋‹ค. ํƒ€์ž…: string (์„ ํƒ์ ) ๊ธฐ๋ณธ๊ฐ’: "localhost"
export default defineConfig({
  server: {
    listen: {
      port: 1028,
      host: "localhost",  // ๋กœ์ปฌ์—์„œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
    },
  },
});
๋ชจ๋“  ๋„คํŠธ์›Œํฌ ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์ˆ˜์‹ :
export default defineConfig({
  server: {
    listen: {
      port: 1028,
      host: "0.0.0.0",  // ์™ธ๋ถ€ ์ ‘๊ทผ ํ—ˆ์šฉ
    },
  },
});
ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ 0.0.0.0์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋ฐฉํ™”๋ฒฝ ์„ค์ •์„ ํ™•์ธํ•˜์„ธ์š”.

fastify

Fastify ์„œ๋ฒ„ ์˜ต์…˜์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: Omit<FastifyServerOptions, "logger"> (์„ ํƒ์ )
export default defineConfig({
  server: {
    fastify: {
      connectionTimeout: 30000,
      keepAliveTimeout: 5000,
      maxParamLength: 200,
      trustProxy: true,  // ํ”„๋ก์‹œ ๋’ค์— ์žˆ์„ ๋•Œ
    },
    // ...
  },
});
๋กœ๊น…์€ Sonamu์˜ logging ์„ค์ •์„ ํ†ตํ•ด ๋ณ„๋„๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

plugins

Fastify ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ™œ์„ฑํ™”ํ•˜๊ณ  ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

formbody

application/x-www-form-urlencoded ์š”์ฒญ ๋ณธ๋ฌธ์„ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: boolean | FastifyFormbodyOptions (์„ ํƒ์ )
export default defineConfig({
  server: {
    plugins: {
      formbody: true,  // ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ํ™œ์„ฑํ™”
    },
  },
});
์˜ต์…˜ ์ง€์ •:
export default defineConfig({
  server: {
    plugins: {
      formbody: {
        bodyLimit: 1048576,  // 1MB
      },
    },
  },
});

qs

์ฟผ๋ฆฌ ์ŠคํŠธ๋ง์„ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค. ์ค‘์ฒฉ๋œ ๊ฐ์ฒด์™€ ๋ฐฐ์—ด์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: boolean | QsPluginOptions (์„ ํƒ์ )
export default defineConfig({
  server: {
    plugins: {
      qs: true,  // ?filter[status]=active&sort[name]=asc ํŒŒ์‹ฑ
    },
  },
});

multipart

ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค (multipart/form-data). ํƒ€์ž…: boolean | FastifyMultipartOptions (์„ ํƒ์ )
export default defineConfig({
  server: {
    plugins: {
      multipart: {
        limits: {
          fileSize: 1024 * 1024 * 30,  // 30MB
          files: 10,  // ์ตœ๋Œ€ 10๊ฐœ ํŒŒ์ผ
        },
      },
    },
  },
});
๋” ํฐ ํŒŒ์ผ ํ—ˆ์šฉ:
export default defineConfig({
  server: {
    plugins: {
      multipart: {
        limits: {
          fileSize: 1024 * 1024 * 100,  // 100MB
          files: 20,
        },
      },
    },
  },
});

static

์ •์  ํŒŒ์ผ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: boolean | FastifyStaticOptions (์„ ํƒ์ )
import path from "path";

export default defineConfig({
  server: {
    plugins: {
      static: {
        root: path.join(import.meta.dirname, "/../", "public"),
        prefix: "/api/public",  // /api/public/* ๊ฒฝ๋กœ๋กœ ์ œ๊ณต
      },
    },
  },
});
์˜ˆ์‹œ: /api/public/images/logo.png โ†’ public/images/logo.png ํŒŒ์ผ

session

์„ธ์…˜ ๊ด€๋ฆฌ๋ฅผ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: boolean | SecureSessionPluginOptions (์„ ํƒ์ )
export default defineConfig({
  server: {
    plugins: {
      session: {
        secret: process.env.SESSION_SECRET || "change-this-secret",
        salt: process.env.SESSION_SALT || "change-this-salt",
        cookie: {
          domain: "localhost",
          path: "/",
          maxAge: 60 * 60 * 24 * 365 * 10,  // 10๋…„
        },
      },
    },
  },
});
ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ secret๊ณผ salt๋ฅผ ์„ค์ •ํ•˜์„ธ์š”!
ํ”„๋กœ๋•์…˜ ์„ค์ •:
export default defineConfig({
  server: {
    plugins: {
      session: {
        secret: process.env.SESSION_SECRET,  // ํ•„์ˆ˜!
        salt: process.env.SESSION_SALT,      // ํ•„์ˆ˜!
        cookie: {
          domain: process.env.COOKIE_DOMAIN,
          path: "/",
          maxAge: 60 * 60 * 24 * 30,  // 30์ผ
          secure: true,  // HTTPS์—์„œ๋งŒ
          httpOnly: true,
          sameSite: "strict",
        },
      },
    },
  },
});

compress

์‘๋‹ต์„ ์••์ถ•ํ•ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๋‚ด์šฉ์€ ๋ณ„๋„ ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”. ํƒ€์ž…: boolean | FastifyCompressOptions (์„ ํƒ์ )
export default defineConfig({
  server: {
    plugins: {
      compress: {
        global: false,  // API๋ณ„๋กœ ์ œ์–ด
        threshold: 1024,  // 1KB ์ด์ƒ๋งŒ ์••์ถ•
        encodings: ["gzip"],
      },
    },
  },
});
โ†’ compress ์ƒ์„ธ ์„ค์ •

cors

CORS(Cross-Origin Resource Sharing)๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: boolean | FastifyCorsOptions (์„ ํƒ์ )
export default defineConfig({
  server: {
    plugins: {
      cors: {
        origin: ["http://localhost:3000", "https://myapp.com"],
        credentials: true,
        methods: ["GET", "POST", "PUT", "DELETE"],
      },
    },
  },
});
๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ๋ชจ๋“  origin ํ—ˆ์šฉ:
export default defineConfig({
  server: {
    plugins: {
      cors: process.env.NODE_ENV === "development" 
        ? { origin: true }  // ๋ชจ๋“  origin ํ—ˆ์šฉ
        : {
            origin: ["https://myapp.com"],
            credentials: true,
          },
    },
  },
});

sse

Server-Sent Events๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: boolean | SsePluginOptions (์„ ํƒ์ )
export default defineConfig({
  server: {
    plugins: {
      sse: true,
    },
  },
});
โ†’ SSE ์‚ฌ์šฉ๋ฒ•

custom

์ปค์Šคํ…€ Fastify ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: (server: FastifyInstance) => void (์„ ํƒ์ )
export default defineConfig({
  server: {
    plugins: {
      custom: (server) => {
        // ์ปค์Šคํ…€ ํ›… ๋“ฑ๋ก
        server.addHook("onRequest", async (request, reply) => {
          console.log(`${request.method} ${request.url}`);
        });
        
        // ์ปค์Šคํ…€ ํ”Œ๋Ÿฌ๊ทธ์ธ ๋“ฑ๋ก
        server.register(myCustomPlugin);
      },
    },
  },
});

apiConfig

API ๋™์ž‘ ๋ฐฉ์‹์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

contextProvider

๊ฐ API ํ˜ธ์ถœ๋งˆ๋‹ค Context๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ํƒ€์ž…: (defaultContext, request) => Context
export default defineConfig({
  server: {
    apiConfig: {
      contextProvider: (defaultContext, request) => {
        return {
          ...defaultContext,
          ip: request.ip,
          session: request.session,
          body: request.body,
        };
      },
    },
  },
});
โ†’ Context ์ƒ์„ธ ์„ค๋ช…

guardHandler

Guard ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์‹คํ–‰ ์‹œ ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ํƒ€์ž…: (guard, request, api) => void | Promise<void>
export default defineConfig({
  server: {
    apiConfig: {
      guardHandler: (guard, request, api) => {
        // ๊ถŒํ•œ ๊ฒ€์‚ฌ ๋กœ์ง
        if (guard === "admin" && !request.session.isAdmin) {
          throw new UnauthorizedError("Admin only");
        }
      },
    },
  },
});

cacheControlHandler

Cache-Control ํ—ค๋”๋ฅผ ์„ค์ •ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ํƒ€์ž…: (req) => string | undefined
import { CachePresets } from "sonamu";

export default defineConfig({
  server: {
    apiConfig: {
      cacheControlHandler: (req) => {
        if (req.type === "assets") {
          return CachePresets.immutable;
        }
        if (req.type === "api" && req.method === "GET") {
          return CachePresets.shortLived;
        }
        return CachePresets.noCache;
      },
    },
  },
});
โ†’ Cache-Control ์ƒ์„ธ ์„ค๋ช…

lifecycle

์„œ๋ฒ„ ๋ผ์ดํ”„์‚ฌ์ดํด ์ด๋ฒคํŠธ์— ํ›…์„ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.

onStart

์„œ๋ฒ„๊ฐ€ ์‹œ์ž‘๋  ๋•Œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: (server: FastifyInstance) => void | Promise<void>
export default defineConfig({
  server: {
    lifecycle: {
      onStart: (server) => {
        console.log(`๐ŸŒฒ Server listening on http://localhost:1028`);
        console.log(`๐Ÿ“Š Routes registered: ${server.printRoutes()}`);
      },
    },
  },
});

onShutdown

์„œ๋ฒ„๊ฐ€ ์ข…๋ฃŒ๋  ๋•Œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค (graceful shutdown). ํƒ€์ž…: (server: FastifyInstance) => void | Promise<void>
export default defineConfig({
  server: {
    lifecycle: {
      onShutdown: async (server) => {
        console.log("Closing database connections...");
        await closeAllConnections();
        console.log("Graceful shutdown complete");
      },
    },
  },
});

onError

์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: (error, request, reply) => void | Promise<void>
export default defineConfig({
  server: {
    lifecycle: {
      onError: (error, request, reply) => {
        console.error(`[ERROR] ${request.method} ${request.url}`, error);
        
        reply.status(500).send({
          error: "Internal Server Error",
          message: process.env.NODE_ENV === "development" 
            ? error.message 
            : undefined,
        });
      },
    },
  },
});

์‹ค์ „ ์˜ˆ์‹œ

๊ธฐ๋ณธ ๊ฐœ๋ฐœ ์„œ๋ฒ„

import path from "path";
import { defineConfig } from "sonamu";

const host = "localhost";
const port = 1028;

export default defineConfig({
  server: {
    baseUrl: `http://${host}:${port}`,
    listen: { port, host },
    
    plugins: {
      formbody: true,
      qs: true,
      multipart: { limits: { fileSize: 1024 * 1024 * 30 } },
      static: {
        root: path.join(import.meta.dirname, "/../", "public"),
        prefix: "/api/public",
      },
      session: {
        secret: "dev-secret-change-in-production",
        salt: "dev-salt-change-in-production",
        cookie: {
          domain: "localhost",
          path: "/",
          maxAge: 60 * 60 * 24 * 365,
        },
      },
    },
    
    apiConfig: {
      contextProvider: (defaultContext, request) => ({
        ...defaultContext,
        ip: request.ip,
        session: request.session,
      }),
    },
    
    lifecycle: {
      onStart: () => console.log(`๐ŸŒฒ Server started at http://${host}:${port}`),
    },
  },
  // ...
});

ํ”„๋กœ๋•์…˜ ์„œ๋ฒ„

import path from "path";
import { defineConfig } from "sonamu";

const port = Number(process.env.PORT ?? 3000);
const host = "0.0.0.0";

export default defineConfig({
  server: {
    baseUrl: process.env.BASE_URL ?? "https://api.myapp.com",
    
    listen: { port, host },
    
    fastify: {
      trustProxy: true,
      connectionTimeout: 60000,
      keepAliveTimeout: 30000,
    },
    
    plugins: {
      compress: {
        global: false,
        threshold: 1024,
        encodings: ["gzip", "br"],
      },
      
      cors: {
        origin: [process.env.FRONTEND_URL ?? "https://myapp.com"],
        credentials: true,
        methods: ["GET", "POST", "PUT", "DELETE"],
      },
      
      formbody: true,
      qs: true,
      
      multipart: {
        limits: {
          fileSize: 1024 * 1024 * 50,  // 50MB
          files: 20,
        },
      },
      
      static: {
        root: path.join(import.meta.dirname, "/../", "public"),
        prefix: "/api/public",
        maxAge: 86400000,  // 1์ผ
      },
      
      session: {
        secret: process.env.SESSION_SECRET!,
        salt: process.env.SESSION_SALT!,
        cookie: {
          domain: process.env.COOKIE_DOMAIN,
          path: "/",
          maxAge: 60 * 60 * 24 * 30,  // 30์ผ
          secure: true,
          httpOnly: true,
          sameSite: "strict",
        },
      },
    },
    
    apiConfig: {
      contextProvider: (defaultContext, request) => ({
        ...defaultContext,
        ip: request.ip,
        session: request.session,
        userAgent: request.headers["user-agent"],
      }),
      
      guardHandler: (guard, request) => {
        if (guard === "admin" && !request.session.isAdmin) {
          throw new UnauthorizedError("Admin access required");
        }
      },
    },
    
    lifecycle: {
      onStart: () => {
        console.log(`๐ŸŒฒ Production server started`);
        console.log(`   Port: ${port}`);
        console.log(`   Base URL: ${process.env.BASE_URL}`);
      },
      
      onShutdown: async () => {
        console.log("Shutting down gracefully...");
      },
      
      onError: (error, request, reply) => {
        console.error(`[ERROR] ${request.method} ${request.url}`, {
          message: error.message,
          stack: error.stack,
        });
        
        reply.status(500).send({
          error: "Internal Server Error",
        });
      },
    },
  },
  // ...
});

Docker ์ปจํ…Œ์ด๋„ˆ ํ™˜๊ฒฝ

import { defineConfig } from "sonamu";

const port = Number(process.env.PORT ?? 3000);

export default defineConfig({
  server: {
    baseUrl: process.env.BASE_URL ?? `http://localhost:${port}`,
    
    listen: {
      port,
      host: "0.0.0.0",  // ์ปจํ…Œ์ด๋„ˆ ์™ธ๋ถ€ ์ ‘๊ทผ ํ—ˆ์šฉ
    },
    
    fastify: {
      trustProxy: true,  // ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ๋’ค์— ์žˆ์„ ๋•Œ
    },
    
    // ... ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ค์ •
  },
  // ...
});

๋‹ค์Œ ๋‹จ๊ณ„

์„œ๋ฒ„ ๊ธฐ๋ณธ ์„ค์ •์„ ์™„๋ฃŒํ–ˆ๋‹ค๋ฉด:
  • auth - ์ธ์ฆ ์„ค์ •
  • storage - ํŒŒ์ผ ์Šคํ† ๋ฆฌ์ง€ ์„ค์ •
  • cache - ์บ์‹œ ์„ค์ •
  • compress - ์‘๋‹ต ์••์ถ• ์„ค์ •