[멋쟁이사자처럼] 프론트엔드 17일차 정리
Next.js 환경에서 클린 아키텍처를 어떻게 적용했는지, 그리고 그 과정에서 무엇을 얻었는지 작성하는 글입니다.
1. Presentation 계층: API Route 핸들러
// app/api/spaces/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { SbSpaceRepository } from '../../infrastructure/repositories/SbSpaceRepository';
import { GetSpaceUsecase } from '../application/usecases/GetSpaceUsecase';
import { createClient } from '../../infrastructure/supabase/server';
import { SbRoomRepository } from '../../infrastructure/repositories/SbRoomRepository';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const spaceId = Number(searchParams.get('spaceId'));
// --- 의존성 주입 (Dependency Injection) ---
const supabase = await createClient();
const spaceRepository = new SbSpaceRepository(supabase);
const roomRepository = new SbRoomRepository(supabase);
const getSpaceUsecase = new GetSpaceUsecase(
spaceRepository,
roomRepository
);
// -----------------------------------------
const spaces = await getSpaceUsecase.execute(spaceId);
return NextResponse.json(spaces);
} catch (error) {
console.error('Error fetching spaces:', error);
return NextResponse.json(
{ error: 'Failed to fetch spaces' },
{ status: 500 }
);
}
}
getSpaceUsecase가 어떻게 데이터를 가져오는지, DB가 Supabase인지 모릅니다. 오직 Usecase를 실행합니다.
2. Application 계층: Usecase
Usecase는 "공간 정보를 가져온다"는 애플리케이션의 특정 동작을 책임집니다. 어떤 Repository를 통해 데이터를 가져올지 알고 있지만, 그 Repository가 어떻게 구현되었는지는 모르며 오직 인터페이스에만 의존합니다.
// application/usecases/GetSpaceUsecase.ts
import { SpaceRepository } from '@/app/api/domain/repository/SpaceRepository';
import { RoomRepository } from '@/app/api/domain/repository/RoomRepository';
import { GetSpaceDto } from '../dtos/GetSpaceDto';
export class GetSpaceUsecase {
// 구현체(SbSpaceRepository)가 아닌 추상화(SpaceRepository)에 의존
constructor(
private spaceRepository: SpaceRepository,
private roomRepository: RoomRepository
) {}
async execute(id: number): Promise<GetSpaceDto> {
// 1. Space 정보를 가져온다.
const space = await this.spaceRepository.findById(id);
// 2. Room 정보를 가져온다.
const rooms = await this.roomRepository.findBySpaceId(id);
// 3. 데이터를 DTO(Data Transfer Object)로 가공하여 반환한다.
const roomInfos = rooms.map((room) => ({
id: room.id,
name: room.name,
// ...
}));
return new GetSpaceDto(space.id, space.name, roomInfos);
}
}
Usecase는 여러 비즈니스 로직을 수행하고, 결과를 외부로 전달할 DTO 형태로 가공합니다.
3. Infrastructure 계층: Repository 구현체
여기가 Supabase와 직접 통신하는부분입니다. Domain 계층에 정의된 SpaceRepository 인터페이스를 실제로 구현합니다.
// infrastructure/repositories/SbSpaceRepository.ts
import { Space } from '../../domain/entities/Space';
import { SpaceRepository } from '../../domain/repository/SpaceRepository';
import { supabaseAdmin } from '../supabase/client';
export class SbSpaceRepository implements SpaceRepository { // 인터페이스 구현
constructor(private supabase: SupabaseClient) {}
async findById(id: number): Promise<Space> {
const { data, error } = await this.supabase
.from('space')
.select('*') // JOIN을 사용한다면 여기에 복잡한 쿼리가 들어갈 수 있음
.eq('id', id)
.single();
if (error) {
throw new Error(`공간 조회 중 오류 발생: ${error.message}`);
}
return data;
}
}
나중에 데이터베이스를 Supabase에서 다른 것으로 바꾸고 싶을 때, 이 파일과 createClient 부분만 수정하면 됩니다. Usecase나 API 핸들러는 건드릴 필요가 없습니다.
클린 아키텍처는 모든 프로젝트에 적용해야 하는 것은 아니라고 생각했으며 작은 규모의 프로젝트에서는 오버 엔지니어링이 될 수 있습니다. 그러나, 프로젝트가 복잡해질 가능성이 있는 경우에는 초기에 이 구조를 도입하는 것이 장기적으로 생산성과 안정성을 얻을 수 있습니다.
단순히 코드를 작성하는 것이 아닌 견고하고 유연한 소프트웨어를 설계하는 경험으로 한 단계 더 성장하는 것을 느꼈습니다.
728x90
'멋쟁이사자처럼' 카테고리의 다른 글
| [멋쟁이사자처럼] 프론트엔드 18일차 정리 (1) | 2025.07.10 |
|---|---|
| [멋쟁이사자처럼] 프론트엔드 16일차 정리 (0) | 2025.07.09 |
| [멋쟁이사자처럼] 프론트엔드 14일차 정리 (0) | 2025.07.05 |
| [멋쟁이사자처럼] 프론트엔드 12일차 정리 (0) | 2025.07.03 |
| [멋쟁이사자처럼] 프론트엔드 11일차 정리 (0) | 2025.07.01 |