SNS 앱 Wathing의 피드 화면을 구현할 때, 클라이언트가 화면 하나를 구성하기 위해 여러 번의 REST API를 호출해야 하는 전형적인 비효율 문제에 직면했습니다.
하나의 '스토리'를 온전히 표시하기 위해 클라이언트는 다음과 같이 평균 5회 이상의 개별 API를 호출하고 그 결과를 직접 조합해야 했습니다.
GET /stories/:idGET /users/:userIdGET /comments?storyId=:idGET /likes?storyId=:idGET /files?storyId=:id이러한 방식은 프론트엔드 개발자에게 복잡한 상태 관리 부담을 안겨주었고, 특히 모바일 환경에서 잦은 네트워크 통신으로 인한 속도 저하와 사용자 경험 저하를 유발했습니다.
이 문제를 근본적으로 해결하기 위해, NestJS 기반의 GraphQL API를 설계하고 도입했습니다.
@ResolveField를 활용한 효율적인 데이터 조합클라이언트가 Story 타입에서 user나 likesCount처럼 직접적인 데이터베이스 컬럼이 아닌 연관 데이터를 요청할 때, 해당 필드 전용 리졸버가 동작하도록 설계했습니다.
이는 서버가 클라이언트의 요청에 맞춰 필요한 데이터를 효율적으로 '조립'해주는 역할을 합니다.
실제 StoryResolver의 구현은 다음과 같습니다.
// src/stories/story.resolver.ts
// ...
@Resolver(() => Story)
export class StoryResolver {
constructor(
private readonly storyService: StoryService,
private readonly usersService: UsersService,
// ... (의존성 주입) ...
) {}
// Story의 'user' 필드가 요청될 때 실행되는 리졸버
@ResolveField(() => User, { description: '스토리를 작성한 사용자' })
async user(@Parent() story: Story): Promise<User> {
// story 객체에서 userId를 가져와 User 정보를 조회합니다.
return await this.usersService.findById(story.userId);
}
// Story의 'likesCount' 필드가 요청될 때 실행되는 리졸버
@ResolveField(() => Int, { description: '좋아요 개수' })
async likesCount(@Parent() story: Story): Promise<number> {
// story ID를 기반으로 좋아요 개수만 계산하여 반환합니다.
return this.storyService.getLikesCount(story.id);
}
// 현재 로그인한 사용자의 '좋아요' 여부를 확인하는 리졸버
@ResolveField(() => Boolean, { description: '사용자가 해당 스토리에 좋아요를 눌렀는지 여부' })
@UseGuards(GqlAuthGuard) // 인증된 사용자만 접근 가능
async hasLiked(
@Parent() story: Story,
@CurrentUser() currentUser: User, // Custom Decorator로 현재 유저 정보 주입
): Promise<boolean> {
return this.storyService.hasLikedStory(story.id, currentUser.id);
}
// ... (다른 ResolveField 및 Mutation들) ...
}
이처럼 각 필드의 데이터 조회 책임을 명확히 분리함으로써, 코드의 재사용성을 높이고 복잡한 비즈니스 로직을 깔끔하게 관리할 수 있었습니다.