본문 바로가기
Next.js

[Next.js + Supabase] API Routes 사용하기ㅣ폴더 구조, 삽질 모음

by 검소한달걀 2025. 2. 28.

 

➜ Next.js + Supabase, 앱 라우터에서 API Routes 사용, API 만들기, 폴더 구조

 

Next.js의 API Routes는 Next.js 내에서 서버 측 API 엔드포인트를 생성해

별도의 백엔드 서버 없이도 풀스택 애플리케이션을 구축할 수 있게 한다.

 

지금 진행하고 있는 토이 프로젝트는 규모가 작고, 복잡한 백엔드 로직이 크게 없을 거 같아서

Next.js로 풀스택 개발을 하기로 결정했다.

 

app/api - route.ts

 

import { createClient } from '@/utils/supabase/server';
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const lat = searchParams.get('lat');
  const lng = searchParams.get('lng');

  const supabase = await createClient();
  const { data: list, error } = await supabase.rpc('get_nearby_pools', {
    latt: Number(lat),
    long: Number(lng),
  });

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }

  return NextResponse.json({ data: list || [] });
}

 

app/api/pool/nearby/route.ts 이 경로로 supabase db에 접근, 함수 호출 동작을 작성했다.

이렇게 하면 API 엔드포인트가 /api/pool/nearby 가 된다.

 

fetch로 API 호출

 

export const getNearbyPools = async (latitude: number, longitude: number) => {
  const response = await fetch(
    `/api/pool/nearby?lat=${latitude}&lng=${longitude}`,
  );
  const { data, error } = await response.json();

  if (!response.ok) {
    throw new Error(error);
  }

  return data;
};

 

그럼 해당 엔드포인트로 api 호출이 가능하고

클라이언트 컴포넌트에서 아래와 같이 함수를 호출해서 사용했다.

 

const fetchNearbyPools = async () => {
  try {
    const pools = await getNearbyPools(coord[0], coord[1]);
    setNearbyPools(pools);
  } catch (error) {
    console.error('nearby pools error', error);
  }
};

 


처음에 API Routes(+ Next.js)를 제대로 이해하지 못하고 냅다 공식 문서 예시와

블로그 글만 보고 사용했다가 꽤 오래 삽질을 했다 ㅎㅎ

 

 

처음엔 데이터 조회 api를 작성하고 서버 컴포넌트인 page.tsx에서 fetch로 호출

// app/api/pools/route.ts 
import { createClient } from '@/utils/supabase/server';
import { NextResponse } from 'next/server';

export async function GET() {
  try {
    const supabase = await createClient();
    const { data: pools } = await supabase
      .from('pools')
      .select('*');

    return NextResponse.json(pools);
  } catch (e) {
    console.error(e);
  }
}


// page.tsx
export default async function Home() {
  const res = await fetch('/api/pools');

  if (!res.ok) {
    throw new Error('Error fetching pools');
  }
}

 

그런데 제대로 동작하지 않고, '/api/pools' 경로에서 앞에 baseURL까지 풀 URL을 추가해 줘야 정상 동작했다.

사용 예시들을 보면 프론트에서 호출 시 엔드포인트 '/api/pools'만으로도 연결이 되던데 왜 안되지?

baseURL 설정을 따로 해줘야되는건가?? 했는데..

이 page.tsx가 서버 컴포넌트이기 때문이었다. 클라이언트 컴포넌트에선 '/api/pools'만으로도 연결이 되지만, 서버 컴포넌트에선 앞에 url을 풀로 적어줘야 동작한다.(근데 이렇게 하면 안 됨)

 

우선

클라이언트 컴포넌트 = 브라우저에서 실행되는 코드

여기서 fetch('/api/pools')라고 쓰면, 브라우저는 그 경로를 현재 실행 중인 웹사이트의 서버로 요청을 보낸다.

서버 컴포넌트 = 서버에서 실행되는 코드

서버 컴포넌트는 브라우저와 달리 현재 서버의 주소를 자동으로 인식하지 않아서 상대 경로인 '/api/pools'로는 요청을 처리할 수 없다.

(서버가 상대 경로를 해석할 수 없어 절대 URL(예: https://example.com/api/pools)로 써줘야 요청을 제대로 보낼 수 있음)

 

즉, Next.js의 API 라우트는 Next.js 서버 내에 존재하는 API 엔드포인트로

브라우저에서 상대 경로를 사용하면, 브라우저는 자동으로 현재 웹사이트의 도메인을 기준으로 API 요청을 보낸다.

반면, 서버 컴포넌트는 브라우저와 다르게 자신이 실행되고 있는 환경을 알지 못하기 때문에

풀 URL을 명시해야만 정확하게 요청을 보낼 수 있다.

 

그러니까 위 방식은 클라이언트 컴포넌트에서 사용하는 거고, 서버 컴포넌트에선 저렇게 하면 안 된다..(불필요)

서버 컴포넌트는 이미 서버에서 실행되고 있기 때문에 여기에서 API 라우트를 호출하는 것은 불필요한 네트워크 홉을 만든다.

 

 

서버 컴포넌트에서 API 라우트 호출 시 

클라이언트 -> 서버 -> API 라우트 (서버 내) -> 데이터베이스 (외부) -> API 라우트 (서버 내) -> 서버 -> 클라이언트

 

  • 클라이언트가 요청을 보냄
  • 서버에서 서버 컴포넌트가 요청을 처리
  • 서버 컴포넌트가 API 라우트를 호출함
  • API 라우트가 외부 데이터베이스와 통신함
  • API 라우트가 데이터를 처리하고 다시 서버 컴포넌트로 반환됨
  • 서버 컴포넌트가 클라이언트에게 응답을 보냄

클라이언트에서 API 라우트 호출 시

클라이언트 -> API 라우트 (서버 내) -> 데이터베이스 (외부) -> API 라우트 (서버 내) -> 클라이언트

 

  • 클라이언트가 직접 API 라우트를 호출
  • API 라우트가 외부 데이터베이스나 다른 서비스와 통신함
  • API 라우트가 데이터를 처리하고 클라이언트에게 응답을 보냄

이렇게 서버 컴포넌트에서 API 라우트를 호출하면,

API 라우트를 다시 호출하는 과정에서 서버 → API 라우트 → 서버로 불필요한 요청 경로가 추가된다.

 

그러므로 서버 컴포넌트에선 바로 supabase를 사용해 직접 데이터 처리를 하는 방식이 더 적절하다.

클라이언트 -> 서버 -> Supabase -> 서버 -> 클라이언트

 

  • 클라이언트가 요청을 보냄
  • 서버가 Supabase에서 데이터 요청
  • Supabase가 데이터를 반환
  • 서버가 데이터를 클라이언트로 반환

 

결론은,

클라이언트 컴포넌트 = API 라우트(/api/...) 사용에 적합

서버 컴포넌트 = db 직접 접근 or 외부 API 통신에 적합

  1.  

이렇게 API Routes 사용과 서버/클라이언트 컴포넌트에서의 api 호출을 명확하게 구분하고자 하니

처음엔 파일이 너무 분리되는 거 같고 지저분해지는 느낌을 받았다..

 

어차피 supabase에 접근하는 거, 엔드 포인트 만들지 말고 그냥 함수로 만들고

사용할 컴포넌트에서 호출해서 쓰면 안 되나? 하는 생각이 들었다.

그래서 맨 위에 작성한 '/api/pool/nearby' 함수 경로를 수정해 API Routes 사용을 하지 않고 직접 호출해 봤다.

 

그랬더니 공개되면 안 되는 키들이 모두 노출되었다.. 😨

 

API 라우트를 거치지 않고 Supabase 호출을 하면,

브라우저에서 직접 요청하므로 모든 요청 정보가 네트워크 탭에 노출된다!!

 

⇣ API 라우트 사용 시


결과적으로,

보안과 코드 관리 측면에서 API 라우트를 통해 서버와 클라이언트와 간의 책임을 명확히 구분하고,

각각의 역할에 충실하도록 하는 것이 정말 중요하다는 걸 다시 한번 느꼈다.

 

폴더 구조에 대해 고민이 많았는데 

각각의 api 호출 함수들은 services 폴더에서 클라이언트/서버를 구분하는 것으로 결정했다.

app/api/pool/nearby/route.ts       - API 엔드포인트
src/services/client/map.ts             - 클라이언트에서 API 호출
src/services/server/map.ts            - 서버 컴포넌트에서 직접 DB 접근

 

 

너무 기본적인 부분들에 대한 이해 부족으로 겪은 삽질이라 ㅎㅎ..

아직 더 많이 공부해야겠지만, 이 과정을 통해 API 라우트 사용과 구조에 대해 좀 더 깊게 이해할 수 있었고

이런 기초적인 보안 문제뿐만 아니라 다방면에서 발생할 수 있는 보안 문제에 대해서도 더 생각해 보게 되었다.

 

 

 

* 잘못된 내용이 있다면 알려주세요 ^_^


참고

공식 문서ㅣRoute Handlers

[Next.js] API 만들어 사용하기(API routes)