본문으로 건너뛰기

15. API 라우트

SvelteKit에서 API 라우트는 풀스택 애플리케이션 구현의 핵심 기능입니다. +server.js 파일을 통해 REST API를 구축하고, 데이터베이스와 연동하며, 인증 시스템을 구현할 수 있습니다. 이 장에서는 API 라우트 구축부터 데이터베이스 연동, 보안까지 실무에서 필요한 모든 내용을 완전히 마스터해보겠습니다.


15.1 REST API 구축

+server.js 파일

SvelteKit에서 API 라우트는 +server.js 파일을 통해 생성됩니다. 이 파일은 HTTP 메서드에 대응하는 함수들을 export하여 요청을 처리하며, Web Request와 Response API를 사용합니다. 각 메서드별로 독립적인 핸들러를 정의할 수 있어 RESTful API 설계가 매우 직관적입니다.

기본 구조

src/routes/api/hello/+server.js
// src/routes/api/hello/+server.js
import { json } from '@sveltejs/kit';

// GET 요청 처리
export async function GET() {
const data = {
message: 'Hello from SvelteKit API!',
timestamp: new Date().toISOString(),
};

return json(data);
}

// POST 요청 처리
export async function POST({ request }) {
const body = await request.json();

return json({
received: body,
processed: true,
});
}

동적 라우트 매개변수

src/routes/api/users/[id]/+server.js
// src/routes/api/users/[id]/+server.js
import { json, error } from '@sveltejs/kit';

// 가상의 사용자 데이터
const users = new Map([
[1, { id: 1, name: '홍길동', email: 'hong@example.com' }],
[2, { id: 2, name: '김철수', email: 'kim@example.com' }],
]);

export async function GET({ params }) {
const userId = parseInt(params.id);
const user = users.get(userId);

if (!user) {
error(404, '사용자를 찾을 수 없습니다');
}

return json(user);
}

export async function PUT({ params, request }) {
const userId = parseInt(params.id);
const updates = await request.json();

const user = users.get(userId);
if (!user) {
error(404, '사용자를 찾을 수 없습니다');
}

const updatedUser = { ...user, ...updates };
users.set(userId, updatedUser);

return json(updatedUser);
}

export async function DELETE({ params }) {
const userId = parseInt(params.id);

if (!users.has(userId)) {
error(404, '사용자를 찾을 수 없습니다');
}

users.delete(userId);

return new Response(null, { status: 204 });
}

실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!

HTTP 메서드 핸들러

SvelteKit은 표준 HTTP 메서드들을 모두 지원하며, 각 메서드별로 최적화된 처리 방식을 제공합니다. Request와 Response 객체를 통해 세밀한 제어가 가능하며, 헤더와 쿠키 관리도 쉽게 할 수 있습니다. json() 헬퍼 함수를 사용하면 JSON 응답을 간편하게 생성할 수 있습니다.

CRUD 작업 구현

src/routes/api/todos/+server.js
// src/routes/api/todos/+server.js
import { json } from '@sveltejs/kit';

let todos = [
{ id: 1, text: 'SvelteKit 학습', completed: false },
{ id: 2, text: 'API 구축', completed: true },
];

let nextId = 3;

// 모든 할 일 조회
export async function GET({ url }) {
const completed = url.searchParams.get('completed');

let filtered = todos;
if (completed !== null) {
filtered = todos.filter(
todo => todo.completed === (completed === 'true')
);
}

return json(filtered);
}

// 새 할 일 생성
export async function POST({ request }) {
const { text } = await request.json();

if (!text || text.trim().length === 0) {
return json(
{ error: '텍스트는 필수입니다' },
{ status: 400 }
);
}

const newTodo = {
id: nextId++,
text: text.trim(),
completed: false,
};

todos.push(newTodo);

return json(newTodo, { status: 201 });
}

파일 업로드 처리

src/routes/api/upload/+server.js
// src/routes/api/upload/+server.js
import { json } from '@sveltejs/kit';

export async function POST({ request }) {
const formData = await request.formData();
const file = formData.get('file');

if (!file || !(file instanceof File)) {
return json(
{ error: '파일이 필요합니다' },
{ status: 400 }
);
}

// 파일 메타데이터 추출
const metadata = {
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
};

// 실제 환경에서는 파일을 저장소에 업로드
const buffer = await file.arrayBuffer();
console.log('파일 크기:', buffer.byteLength);

return json({
message: '파일 업로드 성공',
file: metadata,
});
}

요청과 응답 처리

Request와 Response 객체는 Web API 표준을 따르므로 다른 환경에서도 동일하게 작동합니다. 헤더 관리, 쿠키 설정, 스트리밍 응답 등 다양한 기능을 활용할 수 있습니다. 에러 처리와 상태 코드 관리도 명시적으로 할 수 있어 RESTful API 원칙을 잘 지킬 수 있습니다.

헤더와 쿠키 관리

src/routes/api/auth/login/+server.js
// src/routes/api/auth/login/+server.js
import { json } from '@sveltejs/kit';

export async function POST({ request, cookies }) {
const { email, password } = await request.json();

// 간단한 인증 로직 (실제로는 데이터베이스 확인)
if (
email === 'user@example.com' &&
password === 'password'
) {
// 세션 토큰 생성 (실제로는 JWT 등 사용)
const token = 'session_' + Date.now();

// 쿠키 설정
cookies.set('session', token, {
path: '/',
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 7일
});

return json({
success: true,
user: { email },
});
}

return json({ error: '인증 실패' }, { status: 401 });
}

export async function DELETE({ cookies }) {
cookies.delete('session', { path: '/' });

return json({ success: true });
}

스트리밍 응답

src/routes/api/stream/+server.js
// src/routes/api/stream/+server.js
export async function GET() {
const encoder = new TextEncoder();

const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
const chunk = encoder.encode(
`data: 메시지 ${i}\n\n`
);
controller.enqueue(chunk);

// 1초 대기
await new Promise(r => setTimeout(r, 1000));
}

controller.close();
},
});

return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}

실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!


15.2 데이터베이스 연동

ORM/쿼리 빌더 사용

SvelteKit에서는 Prisma, Drizzle 등 다양한 ORM을 사용할 수 있습니다. Prisma는 타입 안전성과 마이그레이션 도구를 제공하여 데이터베이스 작업을 크게 단순화합니다. 스키마 기반 접근 방식으로 데이터 모델을 명확하게 정의하고 관리할 수 있습니다.

Prisma 설정

prisma/schema.prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "sqlite"
url = "file:./dev.db"
}

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

Prisma 클라이언트 설정

src/lib/server/prisma.js
// src/lib/server/prisma.js
import { PrismaClient } from '@prisma/client';
import { dev } from '$app/environment';

const globalForPrisma = globalThis;

export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: dev ? ['query', 'error', 'warn'] : ['error'],
});

if (dev) globalForPrisma.prisma = prisma;

CRUD 작업

데이터베이스 CRUD 작업은 API 라우트에서 직접 수행할 수 있습니다. Prisma를 사용하면 타입 안전한 쿼리를 작성할 수 있고, 관계형 데이터도 쉽게 처리할 수 있습니다. 에러 처리와 트랜잭션 관리도 간단하게 구현할 수 있습니다.

사용자 CRUD API

src/routes/api/users/+server.js
// src/routes/api/users/+server.js
import { json } from '@sveltejs/kit';
import { prisma } from '$lib/server/prisma';

// 모든 사용자 조회
export async function GET({ url }) {
const includePost =
url.searchParams.get('posts') === 'true';

const users = await prisma.user.findMany({
include: {
posts: includePost,
},
orderBy: {
createdAt: 'desc',
},
});

return json(users);
}

// 새 사용자 생성
export async function POST({ request }) {
const { email, name } = await request.json();

try {
const user = await prisma.user.create({
data: { email, name },
});

return json(user, { status: 201 });
} catch (error) {
if (error.code === 'P2002') {
return json(
{ error: '이미 존재하는 이메일입니다' },
{ status: 400 }
);
}

return json(
{ error: '사용자 생성 실패' },
{ status: 500 }
);
}
}

포스트 CRUD API

src/routes/api/posts/+server.js
// src/routes/api/posts/+server.js
import { json } from '@sveltejs/kit';
import { prisma } from '$lib/server/prisma';

export async function GET({ url }) {
const published = url.searchParams.get('published');

const where = {};
if (published !== null) {
where.published = published === 'true';
}

const posts = await prisma.post.findMany({
where,
include: {
author: true,
},
orderBy: {
createdAt: 'desc',
},
});

return json(posts);
}

export async function POST({ request }) {
const { title, content, authorId } = await request.json();

const post = await prisma.post.create({
data: {
title,
content,
authorId: authorId ? parseInt(authorId) : undefined,
},
include: {
author: true,
},
});

return json(post, { status: 201 });
}

트랜잭션 처리

트랜잭션은 여러 데이터베이스 작업을 원자적으로 처리해야 할 때 필수적입니다. Prisma는 간단한 API로 트랜잭션을 지원하여 데이터 일관성을 보장합니다. 롤백과 에러 처리도 자동으로 처리되어 안전한 데이터 조작이 가능합니다.

src/routes/api/transfer/+server.js
// src/routes/api/transfer/+server.js
import { json, error } from '@sveltejs/kit';
import { prisma } from '$lib/server/prisma';

export async function POST({ request }) {
const { fromUserId, toUserId, postIds } =
await request.json();

try {
const result = await prisma.$transaction(async tx => {
// 포스트 소유권 이전
const updated = await tx.post.updateMany({
where: {
id: { in: postIds },
authorId: fromUserId,
},
data: {
authorId: toUserId,
},
});

if (updated.count === 0) {
throw new Error('이전할 포스트가 없습니다');
}

// 사용자 정보 업데이트
const fromUser = await tx.user.update({
where: { id: fromUserId },
data: { updatedAt: new Date() },
});

const toUser = await tx.user.update({
where: { id: toUserId },
data: { updatedAt: new Date() },
});

return { updated: updated.count, fromUser, toUser };
});

return json(result);
} catch (err) {
error(400, err.message);
}
}

실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!


15.3 인증과 보안

세션 관리

세션 관리는 사용자 인증 상태를 유지하는 핵심 기능입니다. SvelteKit에서는 쿠키 기반 세션이나 JWT 토큰을 사용하여 세션을 관리할 수 있습니다. 서버 사이드에서 세션을 검증하고 클라이언트에 안전하게 전달하는 것이 중요합니다.

세션 기반 인증

src/routes/api/auth/session/+server.js
// src/routes/api/auth/session/+server.js
import { json } from '@sveltejs/kit';
import { prisma } from '$lib/server/prisma';
import crypto from 'crypto';

// 로그인
export async function POST({ request, cookies }) {
const { email, password } = await request.json();

const user = await prisma.user.findUnique({
where: { email },
});

if (!user || !verifyPassword(password, user.password)) {
return json({ error: '인증 실패' }, { status: 401 });
}

// 세션 생성
const sessionId = crypto.randomBytes(32).toString('hex');
const session = await prisma.session.create({
data: {
id: sessionId,
userId: user.id,
expiresAt: new Date(
Date.now() + 7 * 24 * 60 * 60 * 1000
),
},
});

cookies.set('sessionId', sessionId, {
path: '/',
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
});

return json({ user: { id: user.id, email: user.email } });
}

// 로그아웃
export async function DELETE({ cookies }) {
const sessionId = cookies.get('sessionId');

if (sessionId) {
await prisma.session.delete({
where: { id: sessionId },
});
}

cookies.delete('sessionId', { path: '/' });

return json({ success: true });
}

JWT 토큰

JWT는 상태 비저장 인증 방식으로 확장성이 뛰어납니다. 토큰에 사용자 정보를 포함시켜 데이터베이스 조회 없이 인증할 수 있습니다. 리프레시 토큰을 함께 사용하면 보안성과 사용성을 모두 확보할 수 있습니다.

src/routes/api/auth/jwt/+server.js
// src/routes/api/auth/jwt/+server.js
import { json } from '@sveltejs/kit';
import jwt from 'jsonwebtoken';
import { JWT_SECRET } from '$env/static/private';

// 토큰 생성
export async function POST({ request }) {
const { email, password } = await request.json();

// 사용자 인증 (실제로는 데이터베이스 확인)
if (
email !== 'user@example.com' ||
password !== 'password'
) {
return json({ error: '인증 실패' }, { status: 401 });
}

// JWT 토큰 생성
const payload = {
sub: '1',
email,
exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1시간
};

const token = jwt.sign(payload, JWT_SECRET);

return json({ token });
}

// 토큰 검증
export async function GET({ request }) {
const authorization =
request.headers.get('authorization');

if (
!authorization ||
!authorization.startsWith('Bearer ')
) {
return json(
{ error: '토큰이 필요합니다' },
{ status: 401 }
);
}

const token = authorization.slice(7);

try {
const payload = jwt.verify(token, JWT_SECRET);
return json({ valid: true, payload });
} catch (error) {
return json(
{ error: '유효하지 않은 토큰' },
{ status: 401 }
);
}
}

CSRF 보호

CSRF(Cross-Site Request Forgery) 공격을 방지하기 위해 토큰 기반 보호를 구현해야 합니다. SvelteKit의 Form Actions는 기본적으로 CSRF 보호를 제공하지만, API 라우트에서는 직접 구현이 필요합니다. 이중 제출 쿠키나 동기화 토큰 패턴을 사용하여 보안을 강화할 수 있습니다.

src/hooks.server.js
// src/hooks.server.js
import { json } from '@sveltejs/kit';
import jwt from 'jsonwebtoken';
import { JWT_SECRET } from '$env/static/private';

export async function handle({ event, resolve }) {
// JWT 검증 미들웨어
if (event.url.pathname.startsWith('/api/protected')) {
const token = event.cookies.get('token');

if (!token) {
return json(
{ error: '인증이 필요합니다' },
{ status: 401 }
);
}

try {
const payload = jwt.verify(token, JWT_SECRET);
event.locals.user = payload;
} catch (error) {
return json(
{ error: '유효하지 않은 토큰' },
{ status: 401 }
);
}
}

// CSRF 토큰 검증
if (event.request.method !== 'GET') {
const csrfToken =
event.request.headers.get('x-csrf-token');
const sessionCsrf = event.cookies.get('csrf-token');

if (!csrfToken || csrfToken !== sessionCsrf) {
return json(
{ error: 'CSRF 토큰이 유효하지 않습니다' },
{ status: 403 }
);
}
}

const response = await resolve(event);
return response;
}

보호된 API 엔드포인트

src/routes/api/protected/profile/+server.js
// src/routes/api/protected/profile/+server.js
import { json } from '@sveltejs/kit';
import { prisma } from '$lib/server/prisma';

export async function GET({ locals }) {
// hooks.server.js에서 설정한 사용자 정보
const userId = locals.user?.sub;

if (!userId) {
return json(
{ error: '인증이 필요합니다' },
{ status: 401 }
);
}

const user = await prisma.user.findUnique({
where: { id: parseInt(userId) },
select: {
id: true,
email: true,
name: true,
createdAt: true,
},
});

if (!user) {
return json(
{ error: '사용자를 찾을 수 없습니다' },
{ status: 404 }
);
}

return json(user);
}

export async function PATCH({ locals, request }) {
const userId = locals.user?.sub;
const updates = await request.json();

const user = await prisma.user.update({
where: { id: parseInt(userId) },
data: updates,
select: {
id: true,
email: true,
name: true,
},
});

return json(user);
}

실습해보기: Svelte REPL에서 이 코드를 직접 실행해보세요!


정리

SvelteKit의 API 라우트 기능을 완전히 마스터했습니다! 이제 다음과 같은 핵심 개념들을 이해했습니다:

핵심 요약

  • REST API 구축: +server.js 파일과 HTTP 메서드 핸들러로 표준 RESTful API 구현
  • 데이터베이스 연동: Prisma ORM을 통한 타입 안전한 데이터베이스 작업과 트랜잭션 처리
  • 인증과 보안: JWT와 세션 기반 인증, CSRF 보호를 통한 안전한 API 구축

실무 활용 팁

  • API 라우트는 서버 사이드에서만 실행되므로 환경 변수와 민감한 정보를 안전하게 사용
  • Prisma의 타입 생성 기능을 활용하여 프론트엔드와 백엔드 간 타입 일관성 유지
  • hooks.server.js를 활용한 미들웨어 패턴으로 인증과 권한 관리를 중앙화

다음 단계: 16장 "성능 최적화"에서는 빌드 최적화, 런타임 최적화, SSR과 프리렌더링을 통한 성능 향상 방법을 알아보겠습니다. 빠르고 효율적인 SvelteKit 애플리케이션을 만드는 방법을 마스터해보세요!