728x90
반응형

✅ 목적
NestJS 서버에서는 내부 코드를 camelCase, 외부 응답/요청은 snake_case로 통일하기 위해 다음과 같은 구조를 적용하였다.
- 요청(Request):
snake_case → camelCase자동 변환 - 응답(Response):
camelCase → snake_case자동 변환 - 모든 변환은 Interceptor, Pipe 기반으로 구성
📌 요청 변환: SnakeToCamelPipe
클라이언트에서 snake_case로 요청을 보냈을 때, 서버 내부에서는 이를 camelCase로 받아야 한다.
이를 위해 camelcase-keys를 사용한 커스텀 Pipe를 구현하였다.
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
import { default as camelcaseKeys } from 'camelcase-keys';
@Injectable()
export class SnakeToCamelPipe implements PipeTransform {
async transform(value: any, metadata: ArgumentMetadata) {
if (value && typeof value === 'object') {
const containsFile = this.containsFileObject(value);
if (containsFile) return value;
// size_* 패턴 키만 별도 보존 후 camelcase-keys 실행
const sizeKeyPattern = /^size_\d{2,3}_\d{2,3}$/;
const preservedSizeKeys = this.extractSizeKeys(value, sizeKeyPattern);
// 나머지 키 camelCase 변환 (size_* 키 제외)
const camelized = camelcaseKeys(value, {
deep: true,
exclude: [sizeKeyPattern],
});
// 보존한 size_* 키를 다시 덮어씌움
Object.assign(camelized, preservedSizeKeys);
return camelized;
}
return value;
}
private extractSizeKeys(obj: any, pattern: RegExp) {
const result: Record<string, any> = {};
Object.entries(obj).forEach(([key, val]) => {
if (pattern.test(key)) {
result[key] = val;
} else if (typeof val === 'object' && val !== null) {
Object.assign(result, this.extractSizeKeys(val, pattern));
}
});
return result;
}
private containsFileObject(obj: any, seen = new Set()): boolean {
if (obj === null || typeof obj !== 'object') return false;
if (seen.has(obj)) return false;
seen.add(obj);
if (Buffer.isBuffer(obj)) return true;
if ('buffer' in obj && 'originalname' in obj && 'mimetype' in obj) {
return true;
}
if (Array.isArray(obj)) {
return obj.some((item) => this.containsFileObject(item, seen));
}
return Object.values(obj).some((val) => this.containsFileObject(val, seen));
}
}⚠️ size_* 예외 처리 이유
{
"template_id": "1234",
"size_100_120": {...},
"size_80_150": {...}
}자동 변환 시:
| 원래 키 | 자동 변환 결과 |
|---|---|
size_100_120 |
size100120 |
size_80_150 |
size80150 |
이로 인해 다음과 같은 문제가 발생함:
- 클라이언트에서 기대하는 키와 실제 서버에서 처리되는 키가 달라짐
- 직렬화/역직렬화 오류 발생
- 차트 좌표 매핑 로직에서 key mismatch 발생
→ 정규표현식 /^size_\d{2,3}_\d{2,3}$/로 사전 필터링하여 해당 키는 보존 후 다시 병합하는 방식으로 처리
📦 응답 변환: CommonResponseInterceptor
모든 응답은 다음 구조로 통일한다:
{
"code": 200,
"message": "ok",
"data": {
"template_id": "1234",
"created_at": "2024-05-16 14:33:00"
}
}이를 위해 다음과 같은 재귀 변환 함수 사용:
import * as _ from 'lodash';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
function keysToSnakeCase(obj: any): any {
if (obj instanceof Date) {
return dayjs(obj).format('YYYY-MM-DD HH:mm:ss');
}
if (Array.isArray(obj)) {
return obj.map((v) => keysToSnakeCase(v));
}
if (obj && typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
const snakeKey = _.snakeCase(key);
acc[snakeKey] = keysToSnakeCase(obj[key]);
return acc;
}, {});
}
return obj;
}Interceptor 적용 코드:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { SKIP_COMMON_RESPONSE_KEY } from '../decorator/skip-common-response.decorator';
@Injectable()
export class CommonResponseInterceptor<T> implements NestInterceptor<T, any> {
constructor(private readonly reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const skip = this.reflector.getAllAndOverride<boolean>(
SKIP_COMMON_RESPONSE_KEY,
[context.getHandler(), context.getClass()],
);
if (skip) {
return next.handle();
}
return next.handle().pipe(
map((data) => ({
code: 200,
message: 'ok',
data: keysToSnakeCase(data),
})),
);
}
}✅ 정리
| 항목 | 적용 방식 | 위치 |
|---|---|---|
| 요청 snake_case → camelCase | camelcase-keys 사용 |
SnakeToCamelPipe |
| 응답 camelCase → snake_case | lodash.snakeCase 재귀 변환 |
CommonResponseInterceptor |
특수 키(size_*) 예외 처리 |
정규표현식으로 필터 후 병합 | Pipe 내부 |
- 내부 로직은 camelCase로 일관되게 유지
- 외부 통신은 snake_case로 표준화
- 예외 키(
size_100_120)는 자동 변환 시 문제가 발생하므로 수동 처리 - Swagger에서도 예시는 snake_case로 제공하되 내부 DTO는 camelCase로 유지
📌 참고 패키지
728x90
반응형
'Nest.js' 카테고리의 다른 글
| NestJS 엔티티, 왜 다 public이야? – Spring 개발자의 궁금증 해결 (1) | 2025.05.25 |
|---|---|
| [NestJS 인증 흐름] Jwt 토큰 기반 인증 요청 흐름 정리 (1) | 2025.05.22 |
| [Nest.js] 제로베이스에서 1주일, 실전까지 반나절 (1) | 2025.05.22 |
| Nest.js 개발에 필요한 주요 CLI 명령어와 활용법 (1) | 2025.05.22 |
| Nest CLI란 무엇인가? (0) | 2025.05.22 |