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
- 'use server' 지시문
Next.js에 특정 함수가 서버에서만 실행되도록 알려주는 지시문이다. 이로 인해 클라이언트가 이 함수의 내부 로직에 접근할 수 없게 된다. - Server Components
기본적으로 use client 지시문이 없는 컴포넌트는 서버 컴포넌트로 동작하며, 서버에서 렌더링된다. 클라이언트로는 HTML만 전달되기 때문에 초기 렌더링 속도가 빠르다. - 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
- 'use server' 지시문
특정 함수가 서버에서만 실행되도록 지정할 수 있다. 이 함수는 클라이언트에서 접근할 수 없으며, 오직 서버에서 실행된다. page.tsx 같은 페이지 파일에서 'use server' 지시문을 추가하면 서버 사이드에서만 동작해야 할 함수를 선언할 수 있다. - Server Components
파일 최상단에 'use client' 지시문이 없다면, 해당 컴포넌트는 기본적으로 서버 컴포넌트로 동작한다. 서버 컴포넌트는 클라이언트에 HTML만 전송하므로 초기 렌더링이 빠르고, 서버 리소스를 효율적으로 사용할 수 있다. - 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 설정 과정
- 버킷 생성 및 정책 설정: S3 버킷을 생성한 후, 업로드된 이미지에 대한 퍼블릭 접근 권한을 부여했다. 이를 위해 버킷 정책을 추가해 모든 사용자가 이미지를 읽을 수 있도록 설정했다.
- 퍼블릭 접근 설정 주의: S3 버킷에 퍼블릭 읽기 권한을 부여하면 외부에서 이미지를 쉽게 접근할 수 있지만, 이로 인해 발생할 수 있는 보안 이슈를 CloudFront로 보완했다.
CloudFront 설정 과정
CloudFront는 S3 버킷을 원본으로 사용하는 CDN 서비스로, 이를 통해 이미지를 캐싱하고 빠르게 전송할 수 있다.
- 배포 설정: S3 버킷을 원본으로 하는 CloudFront 배포를 생성했다. 이 과정에서 캐싱 규칙을 설정해 자주 변경되지 않는 이미지를 장기간 캐싱하도록 했다.
- 보안 강화: CloudFront를 통해 접근하도록 설정하면 S3 버킷 자체는 비공개로 유지할 수 있다. 이를 통해 S3 URL이 직접 노출되지 않도록 하고, CloudFront URL만 노출되는 방식으로 보안을 강화할 수 있다.
S3와 CloudFront를 함께 사용했을 때의 이점
- 전송 속도 개선: CloudFront는 사용자의 위치에 따라 가장 가까운 엣지 서버에서 이미지를 제공하기 때문에 이미지 로딩 속도가 빨라진다. 특히 글로벌 사용자를 대상으로 할 때 체감 속도 향상이 크다.
- 비용 효율성: S3 버킷에서 직접 제공하는 것보다 CloudFront를 통해 제공하면 S3의 요청 수가 줄어 비용이 절감된다. 특히, 자주 접근하는 이미지가 많다면 CloudFront의 캐싱 기능이 큰 도움이 된다.
- 보안 및 접근 제어: CloudFront URL을 통해서만 접근을 허용하는 방식으로 S3 버킷을 보호할 수 있다. 이렇게 하면 애플리케이션의 모든 이미지 접근이 CloudFront를 통해서 이루어지며, 보안 정책과 로그 관리가 용이해진다.
'프로젝트 > 미니 프로젝트 & 과제' 카테고리의 다른 글
commitlint 설정하기 (0) | 2024.10.30 |
---|---|
[미니 프로젝트] 게시판 프로젝트 17일차 (0) | 2023.06.09 |
[미니 프로젝트] 게시판 프로젝트 15일차 (0) | 2023.06.06 |
[미니 프로젝트] 게시판 프로젝트 13일차 (0) | 2023.06.05 |
[미니 프로젝트] 게시판 프로젝트 11일차 (0) | 2023.06.04 |