저는 공간 예약 트래픽이 한밤중에도 몰리는 SaaS를 운영합니다. REST 중심으로 구축했던 예전 시스템은 폴링이 잦아서 공석이 나도 바로 반영되지 않았고, 동시에 두 명이 같은 시간을 잡으면 뒤늦게 충돌이 일어나곤 했죠. 그래서 이번엔 NestJS + GraphQL + Prisma 조합으로 실시간성을 끌어올렸습니다.
graphql-ws 프로토콜로 통일해 웹·모바일에서 같은 스트림을 바라보게 했습니다.split 링크로 구독과 쿼리를 자연스럽게 나눠 사용자에게 즉각적인 UI 피드백을 줬습니다.GqlConfigService에서 구독 경로를 /graphql 하나로 고정했습니다.onConnect 훅에서 Authorization 헤더를 주입하고, 프론트에서도 동일한 키를 맞췄습니다.src/gql-config.service.ts에서는 다음과 같이 graphql-ws를 등록했습니다.
// src/gql-config.service.ts
subscriptions: {
'graphql-ws': {
path: '/graphql',
onConnect: connectionParams => ({
req: {
headers: {
authorization:
connectionParams.authorization ??
connectionParams.Authorization ??
'',
},
},
}),
},
},
이 구조 덕분에 구독 컨텍스트에서도 기존 JWT 가드를 재사용할 수 있었습니다.
Next.js 앱에서 Apollo Client를 만들 때 다음과 같은 split 구성을 썼습니다.
// src/shared/components/ApolloWrapper/ui/index.tsx
const wsLink = new GraphQLWsLink(
createClient({
url: process.env.NEXT_PUBLIC_GRAPHQL_WS_ENDPOINT ?? 'ws://localhost:4000/graphql',
connectionParams: () => {
const token = localStorage.getItem('accessToken') ?? '';
return token ? { authorization: `Bearer ${token}` } : {};
},
shouldRetry: () => true,
retryAttempts: 5,
}),
);
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
authLink.concat(httpLink),
);
결과적으로 예약이 승인될 때마다 UI 리스트가 스르륵 갱신돼 폴링 코드를 완전히 지울 수 있었습니다.
Prisma 서비스에서는 예약 시간 배열을 비교해 겹치는 슬롯을 막았습니다.
// src/facility-reservations/facility-reservations.service.ts
const occupiedTime = reservation.map(r => r.reservationTime).flat();
const reservationTime = facilityReservation.reservationTime;
const hasCollision = reservationTime.some(time => occupiedTime.includes(time));
if (hasCollision) {
throw new ConflictException('이미 예약된 시간입니다.');
}
upsert가 아닌 분리된 트랜잭션을 쓴 이유는, 한 번의 요청에서 여러 시간대를 잡는 경우가 있어서 실패 시 전체 요청을 롤백하기 위함입니다.
retryWait를 지수 백오프로 바꿔 끊김을 줄였습니다.--filter로 스키마 관련 패키지만 재생성하도록 했습니다. GPT한테는 graphql-ws 재연결 전략을 비교 설명해 달라고 부탁해 최종 파라미터를 결정했습니다.이제 예약 현황을 지켜보던 운영자가 새로고침을 반복하지 않아도 되고, 사용자는 웹과 모바일에서 동일한 실시간 피드를 봅니다. Prisma를 중심으로 검증 로직을 한 곳에 모아두니 REST 시절보다 버그 재현이 쉬워졌고, 로드 테스트에서도 1초 내 응답률이 20% 이상 개선됐습니다.
다음 목표는 예약 승인이 아닌 취소·변경 워크플로에도 같은 실시간성을 적용하는 것입니다. 여러분은 GraphQL 구독을 어디까지 활용하고 계신가요?