main.ts로 이해한 백엔드 부팅 흐름
NestJS의 시작점인 main.ts를 통해 백엔드 서버가 어떻게 부팅되는지 정리한 글
main.ts를 단순히 “서버를 실행하는 파일” 정도로만 생각할 수 있지만 그렇지 않다.
이 파일에서는 Nest 애플리케이션 생성, 요청 데이터 파싱, DTO 유효성 검사, CORS 설정, 포트 실행 같은 백엔드의 공통 정책이 설정된다.
main.ts에서 어떤 흐름으로 서버가 시작되는지 정리해보려고 한다.
흐름 정리
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 요청 body 파싱 설정
// DTO 유효성 검사 전역 적용
// CORS 허용 origin 설정
await app.listen(process.env.PORT ?? 5000);
}
bootstrap();
전체 흐름은 대략 이렇게 이어진다.
src/main.ts
↓
src/app.module.ts
↓
각 기능 모듈
↓
Controller
↓
Service
↓
Prisma
↓
DB
즉, main.ts는 백엔드 서버의 시작점이고, AppModule을 기준으로 프로젝트의 여러 기능 모듈이 연결된다.
1. main.ts는 백엔드 서버의 시작점이다
프론트엔드에서 API 요청을 보내기 전에, 백엔드 서버는 먼저 실행되어 있어야 한다.
예를 들어 이런 요청들이 들어온다고 해보자.
POST /auth/login
GET /users
이 요청들을 처리하려면 백엔드 애플리케이션이 먼저 생성되고, 요청을 받을 수 있는 포트가 열려 있어야 한다.
NestJS에서는 그 시작점 역할을 하는 파일이 보통 src/main.ts다.
처음에는 main.ts를 단순히 실행 파일 정도로만 생각했지만, 실제로는 서버 전체에 적용되는 공통 설정들이 모이는 중요한 진입점이었다.
2. AppModule을 기준으로 Nest 애플리케이션 만들기
main.ts에서 가장 먼저 중요한 코드는 이 부분이다.
const app = await NestFactory.create(AppModule);
NestFactory는 이름 그대로 Nest 애플리케이션을 만들어주는 역할을 한다.
여기서 AppModule은 백엔드 전체의 최상위 모듈이다.
NestJS는 기능을 모듈 단위로 나누는데, 예를 들면 다음과 같은 모듈들이 있을 수 있다.
AuthModule → 인증 관련 기능
UsersModule → 사용자 관련 기능
main.ts는 이 모듈들을 하나하나 직접 실행하지 않는다.
대신 AppModule을 기준으로 Nest 애플리케이션을 생성한다.
이 한 줄이 실행되면 NestJS는 내부적으로 AppModule에 연결된 모듈들을 읽고, 각 모듈 안에 있는 Controller와 Service를 사용할 준비를 한다.
처음에는 이 코드가 바로 서버를 실행하는 줄이라고 생각했다. 하지만 정확히는 서버 객체를 만드는 단계에 가깝다.
외부 요청을 실제로 받기 시작하는 건 아래의 listen() 단계다.
await app.listen(process.env.PORT ?? 5000);
3. 요청 body 크기 제한 설정하기
프로젝트에서는 요청 body 크기를 직접 설정했다.
운영 코드 전체를 공개하지 않고 핵심만 보면 이런 형태다.
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ limit: '50mb', extended: true }));
json()은 JSON 형식의 요청 body를 파싱하기 위한 설정이다.
예를 들어 프론트에서 백엔드로 이런 데이터를 보낼 수 있다.
{
"email": "test@example.com",
"password": "password1234"
}
또는 설문 서비스라면 질문 목록처럼 더 큰 데이터가 들어올 수도 있다.
{
"title": "설문 제목",
"questions": [
"질문 데이터들..."
]
}
요청 body가 서버의 기본 제한보다 크면 요청이 거부될 수 있다. 그래서 JSON 요청과 urlencoded 요청에 대해 최대 크기를 지정했다.
물론 body limit은 무조건 크게 잡는 것이 좋은 것은 아니다.
서비스에서 실제로 필요한 요청 크기를 기준으로 적절히 제한해야 한다.
여기서 urlencoded()는 HTML form이나 일부 도구에서 사용하는 다음과 같은 형식의 데이터를 처리할 때 사용된다.
email=test@example.com&password=password1234
요즘은 JSON 요청을 더 많이 사용하지만, 서버 입장에서는 다양한 요청 형식을 받을 수 있기 때문에 같이 설정해두었다.
4. DTO 유효성 검사를 전역으로 적용하기
백엔드에서 가장 중요하게 느낀 설정 중 하나는 ValidationPipe였다.
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
useGlobalPipes는 특정 API에만 적용하는 것이 아니라, 모든 요청에 공통으로 Pipe를 적용한다는 의미다.
즉, Controller에서 DTO를 사용하는 요청이라면 전역적으로 유효성 검사가 적용된다.
예를 들어 회원가입 DTO가 이런 형태라고 가정해보자.
export class CreateUserDto {
email: string;
password: string;
name: string;
}
그런데 클라이언트가 아래처럼 DTO에 없는 값을 추가해서 보낸다면 어떻게 될까?
{
"email": "test@example.com",
"password": "password1234",
"name": "사용자",
"role": "ADMIN"
}
이때 whitelist: true는 DTO에 정의된 필드만 허용하도록 도와준다.
그리고 forbidNonWhitelisted: true는 DTO에 없는 필드가 들어왔을 때 조용히 제거하는 것이 아니라, 아예 에러를 발생시킨다.
예를 들면 이런 형태의 응답을 받을 수 있다.
{
"message": ["property role should not exist"],
"error": "Bad Request",
"statusCode": 400
}
이 설정은 보안적으로도 중요하다고 느꼈다.
사용자가 요청 body에 임의로 role: ADMIN 같은 값을 넣어 보내더라도, DTO에 정의되지 않은 값이라면 서버 초입에서 막을 수 있기 때문이다.
5. 실제로 만났던 DTO 불일치 에러
이 설정은 실제 개발 중 만났던 에러와도 연결됐다.
설문 문항 구조를 수정하던 중, 프론트에서 보낸 데이터와 백엔드 DTO가 맞지 않아 400 에러가 발생한 적이 있었다.
에러 형태는 대략 이런 식이었다.
{
"message": [
"property someField should not exist"
],
"error": "Bad Request",
"statusCode": 400
}
처음에는 프론트 요청이 문제인지, 백엔드 로직이 문제인지 헷갈렸다.
하지만 원인을 따라가 보니, 프론트에서는 특정 필드를 보내고 있었는데 백엔드 DTO에는 그 필드가 정의되어 있지 않았다.
결국 forbidNonWhitelisted: true 설정 때문에 서버가 요청을 거부한 것이었다.
이 경험을 통해 ValidationPipe가 단순히 형식 검사를 하는 도구가 아니라, 프론트와 백엔드의 데이터 계약을 지켜주는 장치라는 걸 이해하게 됐다.
프론트에서 보내는 요청 구조와 백엔드 DTO 구조가 맞지 않으면 서버가 바로 막아주기 때문이다.
6. transform: true는 왜 필요할까?
ValidationPipe 설정 중에는 transform: true도 있다.
transform: true
HTTP 요청으로 들어오는 값은 기본적으로 문자열인 경우가 많다.
예를 들어 페이지 번호를 query string으로 받는다고 해보자.
GET /posts?page=1
겉으로 보면 1은 숫자처럼 보이지만, 실제 요청에서는 문자열 "1"로 들어올 수 있다.
transform: true를 사용하면 NestJS가 DTO에 정의된 타입에 맞춰 값을 변환하는 데 도움을 준다.
또한 요청 body로 들어온 일반 객체를 DTO 클래스 기준으로 다루는 데에도 도움이 된다.
정리하면 transform: true는 요청 데이터를 DTO 기준에 맞게 정리하는 옵션이라고 볼 수 있다.
다만 모든 값이 자동으로 원하는 타입으로 변환되는 것은 아니기 때문에, 필요한 경우 DTO에서 타입 변환 설정을 함께 고려해야 한다.
7. CORS 허용 주소 설정하기
프론트엔드와 백엔드가 서로 다른 주소에서 실행되면 CORS 설정이 필요하다.
예를 들어 로컬 개발 환경에서는 이런 구조가 될 수 있다.
프론트엔드: http://localhost:3000
백엔드: http://localhost:5000
브라우저는 포트가 달라도 서로 다른 출처로 판단한다.
그래서 백엔드에서 명시적으로 말해줘야 한다.
"이 주소에서 오는 요청은 허용할게."
운영 코드의 실제 주소는 공개하지 않고, 구조만 보면 다음과 같은 방식이다.
const allowedOrigins = [
'http://localhost:3000',
process.env.FRONTEND_URL,
].filter(Boolean);
app.enableCors({
origin: allowedOrigins,
credentials: true,
});
로컬 개발용 주소는 코드에 넣고, 배포된 프론트 주소는 환경변수로 관리했다.
process.env.FRONTEND_URL
이렇게 하면 운영 환경의 실제 주소를 코드에 직접 박아두지 않아도 된다.
또한 .filter(Boolean)을 사용하면 환경변수가 없는 경우 undefined 값이 배열에 들어가는 것을 막을 수 있다.
예를 들어 FRONTEND_URL이 설정되지 않았다면 배열은 원래 이렇게 될 수 있다.
[
'http://localhost:3000',
undefined
]
여기서 .filter(Boolean)을 적용하면 값이 있는 항목만 남는다.
[
'http://localhost:3000'
]
작은 코드지만, 환경변수 누락으로 인한 불필요한 문제를 줄일 수 있는 처리라고 느꼈다.
8. credentials: true는 어떤 의미일까?
CORS 설정에는 credentials: true도 들어간다.
app.enableCors({
origin: allowedOrigins,
credentials: true,
});
이 옵션은 쿠키나 인증 정보를 포함한 요청을 허용할 때 사용된다.
예를 들어 프론트에서 쿠키 기반 인증을 사용한다면 다음과 같은 요청과 관련이 있다.
fetch(url, {
credentials: 'include',
});
현재 프로젝트에서는 Authorization 헤더에 Bearer Token을 담아 보내는 구조가 중심이지만, 인증 관련 요청을 다룰 때 CORS와 credentials 설정이 어떤 의미를 갖는지 이해할 필요가 있었다.
중요한 점은 credentials: true를 사용할 때 아무 origin이나 허용하면 안 된다는 것이다.
그래서 origin: true처럼 무작정 열어두기보다는, 허용할 프론트 주소 목록을 명확히 관리하는 방식이 더 안전하다고 판단했다.
9. 서버 포트 열기
마지막으로 서버를 실제로 실행하는 부분이다.
await app.listen(process.env.PORT ?? 5000);
앞에서 NestFactory.create(AppModule)은 Nest 애플리케이션 객체를 만드는 단계였다.
반면 app.listen()은 외부 요청을 받을 수 있도록 포트를 여는 단계다.
여기서는 환경변수 PORT가 있으면 그 값을 사용하고, 없으면 기본값으로 5000번 포트를 사용한다.
process.env.PORT ?? 5000
이 코드는 다음과 같이 이해할 수 있다.
PORT 환경변수가 있으면 그 값을 사용하고,
없으면 5000번 포트를 사용한다.
로컬 개발 환경에서는 보통 5000번 포트를 사용하고, 배포 환경에서는 실행 환경에서 주입하는 PORT 값을 사용할 수 있다.
10. bootstrap() 호출로 서버 시작하기
마지막 줄에서는 bootstrap() 함수를 호출한다.
bootstrap();
함수는 선언만 해서는 실행되지 않는다.
async function bootstrap() {
// 서버 초기 설정
}
이렇게 함수를 만들어두고, 아래에서 직접 호출해야 실제로 실행된다.
bootstrap();
즉, main.ts의 흐름을 정리하면 다음과 같다.
1. bootstrap 함수 선언
2. Nest 애플리케이션 생성
3. 공통 설정 적용
4. 서버 포트 오픈
5. bootstrap 함수 실행