Next.js app router๋กœ api ํ•จ์ˆ˜ ์ž‘์„ฑํ•˜๊ธฐ

2025. 11. 27. 20:22ใ†Trouble Shooting & Issues/Linkiving

๋ฐ˜์‘ํ˜•



 

๋ชฉ์ฐจ

1. ์ „์ฒด ๊ตฌ์กฐ

2. api.ts : url ์ƒ์ˆ˜

3. handlerServerError.ts : ์„œ๋ฒ„ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ

4. safeFetch.ts : ์•ˆ์ „ํ•œ fetch

5. articles์˜ route.ts


1. ์ „์ฒด ๊ตฌ์กฐ

์šฐ์„  articles์—์„œ ๋‹ค๋ค„์•ผ ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

Next.js์˜ app router์—์„œ๋Š” ๋ณด์ด๋Š” ์Šค์›จ๊ฑฐ ๊ตฌ์กฐ๋ฅผ ๊ทธ๋Œ€๋กœ ํด๋” ๊ตฌ์กฐ๋กœ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.

๋ฌผ๋ก  ์ด ๋•Œ ์ด ํด๋”๋“ค์€ ๋ชจ๋‘ ๊ฐ๊ฐ์˜ route.ts๋ฅผ ํฌํ•จํ•œ๋‹ค.

์š”๋Ÿฌ์ผ€

 

ํด๋” ๊ตฌ์กฐ๋ฅผ ์žก์€ ํ›„์—๋Š” ๊ฐ ํŒŒํŠธ์—์„œ ํ•„์š”ํ•œ ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

articles

• export async function POST() { ... }

export async function GET() { ... }

[articleId]

• export async function GET() { ... }

 export async function PATCH() { ... }

 export async function DELETE() { ... }

like

• export async function POST() { ... }

 export async function DELETE() { ... }

 

์ด ์ค‘ articles์˜ POST๋กœ ์ „์ฒด ๊ตฌ์กฐ๋ฅผ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

import { NextRequest, NextResponse } from 'next/server';
import { API } from '@/constants/api';
import { handlerServerError } from '@/utils/handlerServerError';
import { safeFetch } from '@/utils/safeFetch';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { image, title, content } = body;
    const payload = {
      image,
      title,
      content,
    };

    const data = await safeFetch(`${API.ARTICLES}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload),
    });

    return NextResponse.json({
      message: 'post article',
      data: data,
    });
  } catch (err: unknown) {
    return handlerServerError(err, 'Failed to post articles');
  }
}

 

์šฐ์„  importํ•˜๊ณ  ์žˆ๋Š” API, handlerServerError, safeFetch๋ถ€ํ„ฐ ๋ณด์ž.

 


2. api.ts : url ์ƒ์ˆ˜

constants/api.ts์—์„œ๋Š” url ์ƒ์ˆ˜๋ฅผ ์ •์˜ํ•˜๊ณ  ์žˆ๋‹ค.

const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;

export const API = {
  BASE: BASE_URL,
  USERS: `${BASE_URL}/users/`,
  PROFILE: `${BASE_URL}/profiles/`,
  NOTIFICATION: `${BASE_URL}/notifications/`,
  IMAGE: `${BASE_URL}/images/upload/`,
  COMMENT: `${BASE_URL}/comments/`,
  AUTH: `${BASE_URL}/auth/`,
  ARTICLES: `${BASE_URL}/articles/`,
};

.env ํŒŒ์ผ์— ์„œ๋ฒ„ ์ฃผ์†Œ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ BASE_URL๋กœ ์ฃผ์—ˆ๊ณ , API ๊ฐ์ฒด๋ฅผ ์ •์˜ํ•˜์—ฌ ์Šค์›จ๊ฑฐ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” url๋“ค์„ ์ •์˜ํ–ˆ๋‹ค.

 


3. handlerServerError.ts : ์„œ๋ฒ„ ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ

src/utils/handlerServerError.ts์—์„œ๋Š” ์„œ๋ฒ„ ์—๋Ÿฌ๋ฅผ ๋‹ค๋ฃฌ๋‹ค.

try๋ฌธ์—์„œ ์—๋Ÿฌ๋ฅผ ์žก์•˜์„ ๋•Œ ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ๋ฐ˜๋ณต๋˜์–ด ์ž‘์„ฑํ•ด๋ดค๋‹ค.

import { NextResponse } from 'next/server';

export function handlerServerError(err: unknown, msg: string) {
  if (err instanceof Error) {
    console.error('[ServerError]', err.message); // ์„œ๋ฒ„ ์ฝ˜์†”์— ์ถœ๋ ฅ๋˜๋Š” ์—๋Ÿฌ
    return NextResponse.json({ error: msg }, { status: 500 }); // ํด๋ผ์ด์–ธํŠธ์— ์ถœ๋ ฅ๋˜๋Š” ์—๋Ÿฌ
  } else {
    console.error('[UnknownError]', err);
    return NextResponse.json({ error: msg }, { status: 500 });
  }
}

console.error๋Š” ์„œ๋ฒ„ ์ฝ˜์†”์—๋งŒ ์ถœ๋ ฅ๋˜๊ณ , NextResponse๋Š” ํด๋ผ์ด์–ธํŠธ ์ฝ˜์†”์— ์ถœ๋ ฅ๋œ๋‹ค.

์„œ๋ฒ„ ์—๋Ÿฌ์˜ ๊ฒฝ์šฐ, ํด๋ผ์ด์–ธํŠธ ์ธก์— ๋ณด์ผ ํ•„์š”๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— 

์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” err.message๋Š” console.error๋กœ,

์ง์ ‘ ์ง€์ •ํ•œ ๋ฉ”์„ธ์ง€์ธ msg๋Š” NextResponse๋กœ ๋ณด๋ƒˆ๋‹ค.

 

 


4. safeFetch.ts : ์•ˆ์ „ํ•œ fetch

* fetch๋ž€? : ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์‘๋‹ต์„ ๋ฐ›๋Š” ๋ธŒ๋ผ์šฐ์ €/Node API

src/utils/safeFetch.ts๋Š” fetch ์š”์ฒญ์„ ์•ˆ์ „ํ•˜๊ฒŒ ์ˆ˜ํ–‰ํ•˜๊ณ , ์‹คํŒจ ์‹œ์— ์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธ์šฉ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋ฅผ ๊ตฌ๋ถ„ํ•œ๋‹ค.

export async function safeFetch(url: string, options?: RequestInit) {
  try {
    const res = await fetch(url, options);

    // ์•ˆ์ „ํ•˜๊ฒŒ fetch ์š”์ฒญ ์ˆ˜ํ–‰: ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200~299(์š”์ฒญ ์„ฑ๊ณต)์ธ์ง€ ํ™•์ธ
    if (!res.ok) {
      throw new Error(`Request failed with status ${res.status}`);
    }

    const data = await res.json();
    return data;
  } catch (err: unknown) {
    console.error(err instanceof Error ? err.message : err); // ์„œ๋ฒ„์šฉ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€
    throw new Error('Server request failed'); // ํด๋ผ์ด์–ธํŠธ์šฉ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€
  }
}

์š”์ฒญ url๊ณผ ์˜ต์…˜์„ props๋กœ ๋ฐ›๋Š”๋‹ค.

  • RequestInit : fetch ํ•จ์ˆ˜์— ์˜ต์…˜์„ ์ค„ ๋•Œ ์“ฐ๋Š” ํƒ€์ž…์œผ๋กœ, HTTP ๋ฉ”์„œ๋“œ, ํ—ค๋”, body ๋“ฑ์˜ ์š”์ฒญ ์„ค์ •์„ ๋‹ด๋Š” ๊ฐ์ฒด์ด๋‹ค.
  • res.ok : ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 200~299(์š”์ฒญ ์„ฑ๊ณต)์ธ์ง€ ํ™•์ธํ•œ๋‹ค.
    • ์ฝ”๋“œ๊ฐ€ 200~299 ๋‚ด ๋ฒ”์œ„๋ผ๋ฉด ์š”์ฒญ์€ ์„ฑ๊ณตํ–ˆ๋‹ค๋Š” ๊ฑธ ์˜๋ฏธํ•œ๋‹ค. ์ดํ›„ ์—๋Ÿฌ ๋ฐœ์ƒ์€ ์„œ๋ฒ„ ์—๋Ÿฌ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค.
    • ์ฝ”๋“œ๊ฐ€ 200~299 ์™ธ ๋ฒ”์œ„๋ผ๋ฉด ์š”์ฒญ์ด ์‹คํŒจํ–ˆ๋‹ค๋Š” ๋œป์œผ๋กœ, ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์•Œ๋ฆฐ๋‹ค. (404 ๋“ฑ)
  • data : res.ok๋ฅผ ๋ฌด์‚ฌํžˆ ํ†ต๊ณผํ•˜๋ฉด ์‘๋‹ต์„ data์— ๋„˜๊ธฐ๊ณ  ๋ฆฌํ„ดํ•œ๋‹ค. ์ดํ›„ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋Š” ์„œ๋ฒ„ ์—๋Ÿฌ๋กœ,
    ์„œ๋ฒ„์—๊ฒŒ๋Š” console.error๋กœ ์„œ๋ฒ„์šฉ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด์ด๊ณ , ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ๋Š” Error๋กœ ํด๋ผ์ด์–ธํŠธ์šฉ ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด์ธ๋‹ค.

 

 


5. articles์˜ route.ts

๊ทธ๋Ÿผ ๋‹ค์‹œ articles์˜ route.ts๋ฅผ ๋ณด์ž.

POST : articles

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { image, title, content } = body;
    const payload = {
      image,
      title,
      content,
    };

    const data = await safeFetch(`${API.ARTICLES}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload),
    });

    return NextResponse.json({
      message: 'post article',
      data: data,
    });
  } catch (err: unknown) {
    return handlerServerError(err, 'Failed to post articles');
  }
}

๊ฒŒ์‹œ๊ธ€์„ ๋“ฑ๋กํ•˜๋ฉด articles์— POST๋œ๋‹ค.

๋™์ž‘ ์ˆœ์„œ๋Œ€๋กœ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ์š”์ฒญ์„ ๋ฐ›์•„ body์— ๋„˜๊น€
  2. body๋ฅผ image, title, content๋กœ ๊ฐ์ฒด๊ตฌ์กฐ๋ถ„ํ•ดํ•จ
  3. ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ผ ๊ฐ์ฒด์ธ payload์— image, title, content๋ฅผ ๋ฌถ์–ด์„œ ๋‹ด์Œ
  4. API.ARTICLES ์ฃผ์†Œ๋กœ options์— ๋”ฐ๋ผ safeFetchํ•จ์ˆ˜๋กœ fetch๋ฅผ ์ง„ํ–‰ํ•˜์—ฌ ์‘๋‹ต์„ data์— ์ „๋‹ฌ
  5. NextResponse.json()์— ์˜ˆ์œ ๋ชจ์–‘์œผ๋กœ data๋ฅผ ์ „๋‹ฌ
  6. ์ค‘๊ฐ„์— ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ handlerServerError()๋กœ ์—๋Ÿฌ ์ฒ˜๋ฆฌ (์ด ์—๋Ÿฌ๋“ค์€ ์„œ๋ฒ„ ์—๋Ÿฌ์ž„. ์™œ๋ƒํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ ์—๋Ÿฌ๋Š” safeFech์—์„œ ์žก์Œ)

 

GET : articles

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);

    const page = searchParams.get('page') ?? '1'; // ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ
    const pageSize = searchParams.get('pageSize') ?? '10'; // ํŽ˜์ด์ง€ ๋‹น ๊ฒŒ์‹œ๊ธ€ ์ˆ˜
    const orderBy = searchParams.get('orderBy') ?? 'recent'; // ์ •๋ ฌ ๊ธฐ์ค€
    const keyword = searchParams.get('keyword') ?? ''; // ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ

    const query = new URLSearchParams({
      page,
      pageSize,
      orderBy,
    });
    if (keyword) query.append('keyword', keyword);

    const data = await safeFetch(`${API.ARTICLES}?${query.toString()}`);

    return NextResponse.json({
      message: 'get article',
      data: data,
    });
  } catch (err: unknown) {
    return handlerServerError(err, 'Failed to get articles');
  }
}

์•„๊นŒ POST์—์„œ๋Š” request.json()์œผ๋กœ body๋ฅผ ์–ป์–ด ๊ฐ์ฒด๋ฅผ ๋”ฐ์™”๋‹ค๋ฉด,

1. ์ด๋ฒˆ GET์—์„œ๋Š” request.url๋กœ๋ถ€ํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ฝ์–ด์˜จ๋‹ค.

 

payload๋กœ ์„œ๋ฒ„์— ๋ณด๋‚ผ ๊ฐ์ฒด๋ฅผ ๊ตฌ์„ฑํ–ˆ๋‹ค๋ฉด,

2. query๋กœ ์„œ๋ฒ„์— ์š”์ฒญํ•  ๋ฌธ์ž์—ด์„ ๊ตฌ์„ฑํ•œ๋‹ค.

 

3. API.ARTICLES ๋’ค์— ๊ตฌ์„ฑํ•œ ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด์„ ๋ถ™์—ฌ safeFetch๋กœ ์š”์ฒญ์„ ํ•˜๊ณ , ์‘๋‹ต์„ data์— ๋‹ด๋Š”๋‹ค.

4. data๋ฅผ ์˜ˆ์˜๊ฒŒ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ณด์—ฌ์ค€๋‹ค.

 

 


6. [articleId]์˜ route.ts

์ด๋ฒˆ์—๋Š” articleId๋ฅผ ๋ณด์ž.

articleId๋Š” ์ด์ „๊ณผ ๋‹ค๋ฅด๊ฒŒ id๋ผ๋Š” params๋ฅผ ๊ฐ€์ง„๋‹ค. ๋˜ํ•œ PATCH, DELETE๋ฅผ ๊ฐ€์ง„๋‹ค.

๊ทผ๋ฐ ๋”ฑ ์ด๊ฒƒ๋“ค ๋ง๊ณ ๋Š” article๊ณผ ๊ฑฐ์˜ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ์ž‘์„ฑํ•˜๋ฉด ๋œ๋‹ค.

 

type Params = {
  articleId: string;
};

// articleId ์•ˆ์ „ํ•˜๊ฒŒ number๋กœ ์ „ํ™˜
function parseArticleId(articleId: string) {
  const id = parseInt(articleId, 10);
  if (isNaN(id) || id <= 0) {
    throw new Error('Invalid articleId');
  }
  return id;
}

ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ์— ์•ž์„œ ํŒŒ๋ผ๋ฏธํ„ฐ ํƒ€์ž…๊ณผ ๊ณตํ†ต ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์—ˆ๋‹ค.

params์€ ๋ฌธ์ž์—ด๋กœ ๋“ค์–ด์˜ค๋Š”๋ฐ ์„œ๋ฒ„์—์„œ๋Š” id๋ฅผ number๋กœ ์ •์˜ํ•˜์—ฌ, ์•ˆ์ „ํ•˜๊ฒŒ number๋กœ ๋ฐ”๊ฟ”์ฃผ๋Š” ํ•จ์ˆ˜์ด๋‹ค.

 

GET articleId

export async function GET(_request: NextRequest, { params }: { params: Promise<Params> }) {
  try {
    const { articleId } = await params; //** await์„ ๋ถ™์—ฌ์„œ params๋ฅผ ๋จผ์ € ๊ฐ€์ ธ์™€์•ผ ํ•œ๋‹ค
    const id = parseArticleId(articleId);

    const data = await safeFetch(`${API.ARTICLES}${articleId}`);
    return NextResponse.json({
      message: `ArticleID: ${articleId}`,
      data: data,
    });
  } catch (err) {
    return handlerServerError(err, 'Failed to get article');
  }
}

request๊ฐ€ ํ˜„์žฌ๋Š” ์“ฐ์ด์ง€ ์•Š๊ณ  ์žˆ์–ด์„œ _๋ฅผ ๋ถ™์—ฌ์คฌ๋‹ค.

 

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๊ฒŒ Next.js 15๋ถ€ํ„ฐ๋Š” params๊ฐ€ ๋น„๋™๊ธฐ๋กœ ๋ณ€๊ฒฝ๋˜์–ด(type์ด Promise), params์— ์ ‘๊ทผํ•  ๋•Œ await์œผ๋กœ ๋จผ์ € ์ ‘๊ทผํ•ด์•ผํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

 

 

1. ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ๋ถ€ํ„ฐ articleId๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

2. id๋ฅผ API.ARTICLES ๋’ค์— ๋ถ™์—ฌ์„œ safeFetch๋ฅผ ํ•œ๋‹ค.

3. ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ๋„˜๊ธด๋‹ค.

 

articles์—์„œ๋Š” ์—ฌ๋Ÿฌ ๊ฒŒ์‹œ๊ธ€์„ ์กฐํšŒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํŽ˜์ด์ง€, ์ •๋ ฌ, ๊ฒ€์ƒ‰์–ด๊ฐ™์€ ์ฟผ๋ฆฌํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ url์—์„œ ์ฝ๊ธฐ ์œ„ํ•ด request.url์ด ์‚ฌ์šฉ๋˜์—ˆ๋‹ค.

ํ•˜์ง€๋งŒ articleId๋Š” ๋™์  ๊ฒฝ๋กœ( [articleId] )๋กœ ํ•„์š”ํ•œ Id๋ฅผ ์ด๋ฏธ ๋ฐ›๊ณ  ์žˆ์–ด url์„ ์ฝ์„ ํ•„์š”๊ฐ€ ์—†์–ด request๊ฐ€ ์“ฐ์ด์ง€ ์•Š๋Š”๋‹ค.

 

PATCH articleId

export async function PATCH(request: NextRequest, { params }: { params: Params }) {
  try {
    const articleId = parseArticleId(params.articleId);

    const body = await request.json();
    const { image, title, content } = body;
    const payload = { image, title, content };

    const data = await safeFetch(`${API.ARTICLES}${articleId}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    return NextResponse.json({
      message: `ArticleID: ${articleId} updated`,
      data: data,
    });
  } catch (err: unknown) {
    return handlerServerError(err, 'Failed to patch article');
  }

PATCH๋Š” ๊ธฐ์กด ๋‚ด์šฉ์„ ์ˆ˜์ •ํ•˜๋Š” ๊ฑฐ์—ฌ์„œ request.json()์œผ๋กœ ๊ธฐ์กด ๋‚ด์šฉ์ธ body๋ฅผ ์ฝ์–ด์˜จ๋‹ค.

method๋กœ 'PATCH'๋ฅผ ๋‚ ๋ ค์„œ ์„œ๋ฒ„ํ•œํ…Œ ์ด ์š”์ฒญ์ด ์ˆ˜์ • ์š”์ฒญ์ž„์„ ์•Œ๋ฆฐ๋‹ค.

 

 

DELETE articleId

export async function DELETE(_request: NextRequest, { params }: { params: Params }) {
  try {
    const articleId = parseArticleId(params.articleId);
    const data = await safeFetch(`${API.ARTICLES}${articleId}`, { method: 'DELETE' });

    return NextResponse.json({
      message: `ArticleID: ${articleId} deleted`,
      data: data,
    });
  } catch (err) {
    return handlerServerError(err, 'Failed to delete article');
  }
}

delete๋Š” method๋กœ 'DELETE'๋ฅผ ๋‚ ๋ ค์„œ ์‚ญ์ œ ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค.

 

 

 

 

์ฐธ๊ณ 

๋ฐ˜์‘ํ˜•