기술 포스트

프론트만 하다가 API Route를 만져봤다


Article

지금까지 API는 항상 "받는 쪽"이었다. 백엔드 개발자가 만들어준 엔드포인트에 fetch를 날리고, 응답을 화면에 뿌리는 것까지가 내 일이었다. 이번에 사이드 프로젝트에 문의 폼을 붙이면서 처음으로 "만드는 쪽"에 서봤다. Next.js의 Route Handler 덕분이다.

API Route는 그냥 파일 하나다

거창한 서버 세팅을 상상했는데, 실체는 app 폴더 안에 route.ts 파일 하나 만드는 게 전부였다.

// app/api/contact/route.ts
export async function POST(request: Request) {
  const { name, email, message } = await request.json();

  if (!email || !message) {
    return Response.json({ error: "필수 항목 누락" }, { status: 400 });
  }

  // 메일 발송 서비스에 전달
  await sendMail({ name, email, message });

  return Response.json({ ok: true });
}

POST라는 이름의 함수를 export하면 그게 곧 POST 엔드포인트가 된다. Request를 받아서 Response를 돌려준다. 프론트에서 매일 쓰던 fetch의 반대편을 그대로 뒤집어 놓은 모양이라, 낯설 줄 알았던 문법이 오히려 익숙했다. 백엔드가 거창한 게 아니라 프론트의 연장이구나 싶었던 순간이다.

진짜 배운 건 보안 기본기

두 번째로 만든 건 외부 API 프록시였다. 유튜브 데이터를 가져오는 기능인데, 처음엔 클라이언트에서 바로 호출했다.

// ❌ 이러면 API 키가 브라우저에 노출된다
fetch(`https://api.example.com/videos?key=${API_KEY}`);

클라이언트 코드는 전부 사용자에게 배달된다. 개발자 도구 네트워크 탭만 열어도 키가 그대로 보인다. 그래서 서버를 한 번 거치게 바꿨다.

// app/api/videos/route.ts
export async function GET() {
  const res = await fetch(
    `https://api.example.com/videos?key=${process.env.API_KEY}`,
    { next: { revalidate: 3600 } },
  );

  if (!res.ok) {
    return Response.json([], { status: 200 }); // 실패 시 빈 배열
  }

  const data = await res.json();
  return Response.json(data);
}

클라이언트는 /api/videos만 호출하고, 키는 서버의 환경변수 안에만 존재한다. 토큰과 시크릿은 절대 브라우저로 내려보내지 않는다는 원칙 하나를 몸으로 이해한 게 이번 작업의 가장 큰 수확이다. NEXT_PUBLIC_ 접두사가 붙은 환경변수만 클라이언트에 노출된다는 Next.js의 규칙도 이제야 왜 그렇게 설계됐는지 납득이 된다.

프론트 하던 감각이 그대로 쓰인다

며칠 만져본 소감을 정리하면, 요청/응답 구조는 이미 알고 있었다. fetch를 쓰는 쪽에서 수백 번 봤던 상태 코드, 헤더, JSON이 그대로 나온다. 에러 처리 습관도 그대로 통해서, 응답이 실패하면 빈 배열을 돌려주는 패턴은 프론트에서 하던 방어 코드와 같은 사고방식이다. 다만 책임의 무게는 다르다. 클라이언트 버그는 그 사용자 화면에서 끝나지만, 서버에서 키가 새면 전체가 뚫린다.

물론 이 정도로 백엔드를 안다고 말할 수는 없다. DB 설계, 인증, 스케일링 같은 세계는 아직 문 밖이다. 그래도 폼 전송과 프록시 정도는 이제 남의 손을 빌리지 않고 만들 수 있다. 경계선 하나를 넘은 느낌이고, 이 느낌이 꽤 오래갈 것 같다.

Share
  • Next.js
  • API
  • 백엔드
프론트만 하다가 API Route를 만져봤다 — 디코드랩(DCODELAB)