프로젝트/미니 프로젝트 & 과제

[meetup-now] 이벤트 생성 폼 만들고 이미지 업로드 기능 구현하기

JeanneLee57 2024. 11. 3. 18:58

Next.js와 Supabase로 이벤트 초대장 애플리케이션 만들기

이벤트를 생성하면 그 이벤트에 대한 초대장을 만들어주고 공유할 수 있는 서비스를 개인 프로젝트로 진행하고 있다. 단순 초대장 생성 기능뿐 아니라 내 위치 기반으로 주변의 이벤트들을 확인하고 참가할 수 있는 기능도 차차 추가해 보려고 한다. 이벤트 생성 기능과 관련 이미지를 업로드할 수 있는 기능을 구현했다. 프론트엔드에는 Next.js, 백엔드에는 Supabase를 사용하면서 S3와 CloudFront로 이미지를 호스팅하는 방식을 적용해봤다. 이 과정에 대해서 기록해 보려고 한다.

Supabase에서 이벤트 테이블 만들기

먼저, 이벤트 데이터를 저장할 event 테이블을 Supabase에서 만들었다. 테이블은 간단히, 이벤트 제목, 날짜 및 시간, 위치, 주최자, 설명, 공개 여부, 그리고 이미지 URL을 포함하는 형태로 구성했다.

Supabase는 RDBMS로 PostgreSQL을 기반으로 하기 때문에 다양한 데이터 타입을 제공하고, 특히 JSON 데이터도 쉽게 처리할 수 있는 점이 강점이다. 이를 통해 복잡한 데이터 구조도 무리 없이 저장할 수 있다는 것을 이번에 실제로 사용해보며 알게 되었다.

테이블 생성 후에는 바로 Next.js의 Server Components와 Supabase 클라이언트를 활용해 데이터베이스 연결을 설정했다. Next.js 13부터는 기존에 사용하던 API 라우트를 대체할 수 있는 여러 서버 관련 기능이 제공되기 때문에, API 경로를 만들지 않고도 원하는 서버 사이드 로직을 처리할 수 있게 되었다.

Next.js Server Components와 Server Actions로 서버 역할 구현하기

이벤트 생성 폼을 Next.js와 연결하면서 서버 역할이 어떻게 동작하는지 궁금해졌다. 이번에 진행했던 Next.js 프로젝트에서는 pages/api 경로에 서버 엔드포인트를 만들고 프론트엔드에서 이 엔드포인트로 데이터를 전송하는 방식으로 서버를 구현했었다. 하지만 Next.js 13부터는 Server Components와 Server Actions라는 기능 덕분에 굳이 별도의 API 엔드포인트를 만들지 않아도 서버 사이드 로직을 처리할 수 있게 되었다.

Server Components와 Server Actions

  1. 'use server' 지시문
    Next.js에 특정 함수가 서버에서만 실행되도록 알려주는 지시문이다. 이로 인해 클라이언트가 이 함수의 내부 로직에 접근할 수 없게 된다.
  2. Server Components
    기본적으로 use client 지시문이 없는 컴포넌트는 서버 컴포넌트로 동작하며, 서버에서 렌더링된다. 클라이언트로는 HTML만 전달되기 때문에 초기 렌더링 속도가 빠르다.
  3. Server Actions
    Server Action은 폼의 action 속성에 지정된 서버 함수로, 폼이 제출될 때 서버에서 함수가 자동으로 실행된다. 예를 들어 createEvent라는 서버 함수를 form action={createEvent}으로 지정하면, 서버 액션이 자동으로 실행되므로 API 라우트를 만들 필요가 없다.
// form action을 통한 Server Action 사용 예시
<form action={createEvent}> {/* form fields */} </form>

 

 

이처럼 Next.js 13에서는 'Server Components'와 'Server Actions'라는 새로운 개념을 도입해, 별도의 API 엔드포인트 없이도 서버 사이드 로직을 처리할 수 있다. 이를 통해 서버 관련 로직을 페이지 컴포넌트 내에서 바로 구현하거나, 서버 액션을 통해 특정 함수를 서버에서 실행할 수 있게 되어, 기존의 API 라우트 작성 방식이 달라졌다.

기존 방식과 새로운 방식 비교

기존에는 Next.js에서 서버 관련 작업을 수행하려면 pages/api 디렉토리에 API 라우트를 생성한 후, 클라이언트에서 이 API 엔드포인트로 요청을 보내는 방식이었다. 이제는 Next.js 13의 Server Components와 Server Actions를 활용해 이러한 작업을 더 단순하게 구현할 수 있다.

Server Components와 Server Actions

  1. 'use server' 지시문
    특정 함수가 서버에서만 실행되도록 지정할 수 있다. 이 함수는 클라이언트에서 접근할 수 없으며, 오직 서버에서 실행된다. page.tsx 같은 페이지 파일에서 'use server' 지시문을 추가하면 서버 사이드에서만 동작해야 할 함수를 선언할 수 있다.
  2. Server Components
    파일 최상단에 'use client' 지시문이 없다면, 해당 컴포넌트는 기본적으로 서버 컴포넌트로 동작한다. 서버 컴포넌트는 클라이언트에 HTML만 전송하므로 초기 렌더링이 빠르고, 서버 리소스를 효율적으로 사용할 수 있다.
  3. Server Actions
    Server Actions는 폼의 action 속성에서 호출되는 서버 측 함수다. 폼이 제출될 때 서버에서 직접 함수가 실행되므로 별도의 API 라우트를 만들 필요가 없다. 예를 들어, createEvent라는 서버 액션을 정의하면, form action={createEvent}을 통해 자동으로 서버에서 함수를 실행할 수 있다.
 

이 방식의 주요 장점은 클라이언트와 서버 사이의 데이터 전송 로직을 줄일 수 있다는 점이다. 그러나 Next.js App Router 환경에서만 작동한다는 점과, 환경 변수 및 서버 전용 라이브러리(Supabase 클라이언트 등)는 반드시 서버에서만 사용되어야 한다는 제약이 있다.

서버 액션 코드 작성: 역할에 따른 코드 분리 전략

Server Actions를 사용하면 API 라우트를 생략할 수 있지만, 코드가 복잡해질 때는 역할별로 분리하는 것이 유지보수에 유리하다. 지금은 서버 요청과 관련된 코드가 많지 않아서 한 곳에 코드를 몰아 두어도 괜찮지만, 앞으로 확장될 것을 고려해서 역할에 따라 코드를 액션과 서비스 레이어로 분리해 두기로 했다.

1. 서버 액션을 별도 파일로 분리하기

서버 액션을 별도의 파일로 분리하여 eventActions.ts로 관리할 수 있다. 이렇게 하면 주요 비즈니스 로직과 서버 액션 로직을 각각의 파일로 나누어 관리하기 쉽다.

// eventActions.ts - 서버 액션을 별도 파일로 분리
'use server';

export async function createEvent(eventData: EventData) {
  const { error } = await supabase.from('event').insert(eventData);
  if (error) throw new Error('이벤트 생성에 실패했습니다');
}

2. 페이지 컴포넌트에서 서버 액션 호출하기

서버 액션을 호출하는 페이지 컴포넌트는 깔끔해지며, 각 파일이 하나의 역할만 수행하도록 설계할 수 있다.

// page.tsx
import { createEvent } from './eventActions';

export default function EventPage() {
  return (
    <form action={createEvent}>
      {/* form fields */}
    </form>
  );
}

3. 서비스 레이어 추가로 로직 분리하기

비즈니스 로직이 복잡해질 경우, eventService.ts라는 서비스 레이어 파일을 추가하여 서버 액션에서 이를 호출하는 구조로 분리할 수 있다.

// eventService.ts - 서비스 레이어로 비즈니스 로직 분리
export async function addEventToDatabase(eventData: EventData) {
  const { error } = await supabase.from('event').insert(eventData);
  if (error) throw new Error('데이터베이스에 이벤트 추가 실패');
}

// eventActions.ts - 서버 액션에서 서비스 호출
import { addEventToDatabase } from './eventService';

export async function createEvent(eventData: EventData) {
  await addEventToDatabase(eventData);
}

이러한 구조의 장점은 다음과 같다:

  • 관심사 분리: 각 파일이 하나의 역할을 담당해 가독성과 유지보수성을 높일 수 있다.
  • 재사용성: 동일한 로직을 여러 곳에서 사용할 때, 서비스 레이어의 함수를 호출함으로써 코드 중복을 줄일 수 있다.
  • 테스트 용이성: 비즈니스 로직을 분리해 유닛 테스트가 쉬워진다.
  • 유지보수성: 코드 구조가 명확히 구분되어, 코드 수정 시 영향을 받는 부분을 쉽게 파악할 수 있다.

API 라우트가 유용한 경우

Server Actions가 강력하지만, 여전히 API 라우트가 필요한 경우가 있다. 다음과 같은 상황에서는 API 라우트 사용을 고려할 수 있다.

  • 외부 API 제공: 다른 애플리케이션이 접근할 수 있는 API를 제공해야 할 때
  • 웹훅 처리: 특정 이벤트 발생 시 서버에서 처리해야 하는 경우
  • REST API 필요: 프론트엔드와 서버 사이에서 데이터 통신이 빈번한 경우
  • 클라이언트 사이드에서만 데이터 가져오기 필요: 클라이언트에서만 데이터를 요청해야 할 때

S3와 CloudFront로 이미지 업로드 구현하기

이벤트와 관련한 이미지를 업로드하기 위해 Amazon S3와 CloudFront를 연계했다. 이는 이미지 로딩 속도와 보안, 비용 측면에서 최적화된 구조를 제공해준다.

S3 설정 과정

  1. 버킷 생성 및 정책 설정: S3 버킷을 생성한 후, 업로드된 이미지에 대한 퍼블릭 접근 권한을 부여했다. 이를 위해 버킷 정책을 추가해 모든 사용자가 이미지를 읽을 수 있도록 설정했다.
  2. 퍼블릭 접근 설정 주의: S3 버킷에 퍼블릭 읽기 권한을 부여하면 외부에서 이미지를 쉽게 접근할 수 있지만, 이로 인해 발생할 수 있는 보안 이슈를 CloudFront로 보완했다.

CloudFront 설정 과정

CloudFront는 S3 버킷을 원본으로 사용하는 CDN 서비스로, 이를 통해 이미지를 캐싱하고 빠르게 전송할 수 있다.

  1. 배포 설정: S3 버킷을 원본으로 하는 CloudFront 배포를 생성했다. 이 과정에서 캐싱 규칙을 설정해 자주 변경되지 않는 이미지를 장기간 캐싱하도록 했다.
  2. 보안 강화: CloudFront를 통해 접근하도록 설정하면 S3 버킷 자체는 비공개로 유지할 수 있다. 이를 통해 S3 URL이 직접 노출되지 않도록 하고, CloudFront URL만 노출되는 방식으로 보안을 강화할 수 있다.

S3와 CloudFront를 함께 사용했을 때의 이점

  • 전송 속도 개선: CloudFront는 사용자의 위치에 따라 가장 가까운 엣지 서버에서 이미지를 제공하기 때문에 이미지 로딩 속도가 빨라진다. 특히 글로벌 사용자를 대상으로 할 때 체감 속도 향상이 크다.
  • 비용 효율성: S3 버킷에서 직접 제공하는 것보다 CloudFront를 통해 제공하면 S3의 요청 수가 줄어 비용이 절감된다. 특히, 자주 접근하는 이미지가 많다면 CloudFront의 캐싱 기능이 큰 도움이 된다.
  • 보안 및 접근 제어: CloudFront URL을 통해서만 접근을 허용하는 방식으로 S3 버킷을 보호할 수 있다. 이렇게 하면 애플리케이션의 모든 이미지 접근이 CloudFront를 통해서 이루어지며, 보안 정책과 로그 관리가 용이해진다.