모듈 연결과 실행
프로젝트·파일·폴더 모듈을 AppModule에 등록하고 NestJS 서버를 실행합니다.
Project, File, Folder Modules
각 기능을 모듈로 묶고 AppModule에 등록합니다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Project } from './entities/project.entity';
import { ProjectService } from './project.service';
import { ProjectController } from './project.controller';
import { File } from '../file/entities/file.entity'; // File 엔티티 임포트
import { Folder } from '../folder/entities/folder.entity'; // Folder 엔티티 임포트
@Module({
imports: [
TypeOrmModule.forFeature([Project, File, Folder]), // 프로젝트, 파일, 폴더 엔티티 등록
],
controllers: [ProjectController],
providers: [ProjectService],
exports: [ProjectService], // 필요한 경우 ProjectService 내보내기
})
export class ProjectModule {}// import { Module } from '@nestjs/common';
// import { TypeOrmModule } from '@nestjs/typeorm';
// import { File } from './entities/file.entity';
// @Module({
// imports: [TypeOrmModule.forFeature([File])],
// providers: [],
// exports: [],
// })
// export class FileModule {}마찬가지로 folder.module.ts도 만듭니다.
현재는 ProjectModule에서 File과 Folder 엔티티를 관리하고 있으므로, 별도의 모듈은 당장 필요 없습니다.
하지만 DDD 관점에서 나중에 파일과 폴더를 자체 도메인으로 분리할 때 유용합니다.
TypeOrmModule.forRoot()는 DB 연결과 연결에서 사용할 엔티티를 정합니다. forFeature([Project, File, Folder])는 해당 모듈에서 주입할 저장소를 등록합니다. 같은 배열처럼 보여도 역할이 다릅니다. 이 예제의 ProjectService가 세 저장소를 주입받으므로 모두 forFeature에 들어갑니다. 서비스의 providers 등록과 다른 모듈에서 사용할 때 필요한 exports도 따로 읽습니다.
AppModule에 ProjectModule 등록
src/app.module.ts에 ProjectModule을 임포트합니다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { ProjectModule } from './project/project.module'; // ProjectModule 임포트
import { User } from './auth/entities/user.entity'; // 엔티티 추가
import { Project } from './project/entities/project.entity'; // 엔티티 추가
import { File } from './file/entities/file.entity'; // 엔티티 추가
import { Folder } from './folder/entities/folder.entity'; // 엔티티 추가
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, envFilePath: process.env.NODE_ENV === 'development' ? '.env.development' : '.env' }),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
entities: [User, Project, File, Folder], // 모든 엔티티 등록
synchronize: true,
logging: ['query', 'error'],
}),
AuthModule,
ProjectModule, // ProjectModule 등록
],
controllers: [],
providers: [],
})
export class AppModule {}주의: entities 배열에 모든 엔티티를 명시적으로 추가해야 TypeORM이 해당 엔티티를 인식하고 테이블을 생성합니다.
최종 코드의 synchronize: true도 앞 설정과 같은 개발용 전제입니다. 기존 운영 DB를 이 설정으로 갱신하는 실습은 하지 않습니다. 인증 모듈과 프로젝트 모듈을 붙여도 생략한 DTO, 요청 검증, DB 접속과 CORS 설정이 자동으로 완성되지는 않습니다.
NestJS 애플리케이션 실행
이제 NestJS 애플리케이션을 실행하고 API를 테스트할 수 있습니다.
npm run start:dev서버가 3000번 포트에서 실행되면, Postman이나 Insomnia 같은 API 클라이언트를 사용하여 구현한 API를 테스트할 수 있습니다.
테스트 시나리오POST /auth/register로 회원가입합니다. 이메일·비밀번호와 함께 예제의 nickname도 보내고, 성공 응답의 message를 확인합니다.
POST /auth/login으로 로그인 (회원가입한 이메일, 비밀번호) -> accessToken 받기
GET /auth/profile로 프로필 조회 (Header: Authorization: Bearer <accessToken>)
POST /projects로 새 프로젝트 생성 (Header: Authorization: Bearer <accessToken>, Body: { "name": "My First Project" })
GET /projects로 내 프로젝트 목록 조회 (Header: Authorization: Bearer <accessToken>)
GET /projects/:projectId로 특정 프로젝트 상세 조회
앞 단계에서 받은 projectId로 POST /projects/:projectId/files를 호출합니다. 본문은 { "name": "main.ts", "path": "/main.ts", "type": "typescript", "content": "console.log(1);" }로 시작해 보세요. 정상 생성 응답에서 파일 id를 기록합니다.
기록한 fileId로 GET /projects/:projectId/files/:fileId를 호출합니다. 응답의 content가 console.log(1);인지 확인합니다.
PATCH /projects/:projectId/files/:fileId/content에 { "content": "console.log(2);" }를 보냅니다. 성공 응답만 보고 끝내지 말고 같은 파일을 다시 GET하여 content가 바뀌었는지 확인합니다. 이 단계는 REST 저장이며, 다른 브라우저에 변경을 전파하지 않습니다.
토큰 없이 보호 API를 호출하면 401 응답을 예상합니다. 다른 사용자의 프로젝트나 파일에도 접근할 수 없어야 합니다. 요청이 실패하면 먼저 토큰·기록한 ID·요청 경로를 확인하고, 서버가 시작되지 않으면 앞 절의 패키지·DTO·DB 설정을 점검합니다. 이 목록은 확인 절차이며, 예제의 실행 성공을 미리 보장하는 테스트 결과는 아닙니다.
연결 확인 문제
엔티티 네 개가 forRoot에 있지만 ProjectModule의 forFeature에서 File만 빠졌습니다. ProjectService의 파일 저장소 주입과 기존 파일 생성 요청 중 어느 단계부터 살펴봐야 하나요? 또 ProjectService를 같은 모듈에서 쓰는 것과 RealtimeModule에서 쓰는 경우에 exports가 왜 다르게 필요한가요?
모듈 경계로 답 확인
먼저 시작 시 저장소 주입과 모듈 등록을 확인합니다. DB 연결이 그 엔티티를 알고 있다는 것만으로 해당 모듈에 필요한 저장소 provider 등록까지 대신하지는 않습니다. 같은 모듈의 컨트롤러는 그 모듈에 등록된 서비스를 사용합니다. 다른 모듈의 Gateway에서 사용하려면 서비스를 제공하는 모듈이 내보내고 사용하는 모듈이 그 모듈을 import하는 연결이 필요합니다. 요청 본문을 바꾸는 것만으로 주입 단계의 누락이 해결되지는 않습니다.
실전 구현 단계에서는 각 모듈이 어떤 경계 계약을 책임지는지 먼저 고정하면, 인증과 파일 관리 API를 확장할 때 권한 누락과 데이터 불일치를 줄일 수 있습니다.
이번 절에서는 온라인 코드 에디터 프로젝트의 백엔드 API를 구축하는 초기 단계를 진행했습니다.
사용자 인증, 프로젝트 및 파일/폴더 관리를 위한 기본 RESTful API를 TypeORM, Passport-JWT를 활용하여 구현했습니다.
다음 프런트엔드 통합에서는 React 인증 상태와 프로젝트 화면을 이 REST API에 연결합니다. API로 파일 하나를 준비해 두면 에디터에서 조회·편집·자동 저장을 확인할 수 있습니다. 같은 소유 계정의 두 화면에 변경을 전달하고 싶다면 선택 절 같은 파일의 두 세션 연결하기에서 Gateway를 추가합니다. 기본 REST 저장에 이 모듈이 필요한 것은 아닙니다.
NestJS 프로젝트 초기 설정과 데이터베이스 설정을 중심으로 정리한 보조 다이어그램입니다.
Nest · Module assembly
AppModule과 ProjectModule 등록
최종 AppModule의 네 imports와 ProjectModule의 저장소·controller·provider·export를 대조합니다. File·Folder 저장소는 이 예제에서 ProjectModule에 함께 등록됩니다.
- AppModule —
imports- ConfigModule.forRoot
환경 파일 선택 ·
isGlobal: true - TypeOrmModule.forRoot
PostgreSQL ·
synchronize: trueentities: User, Project, File, Folder - AuthModule
앞 절의 사용자 인증 모듈
- ProjectModule
imports:TypeOrmModule.forFeature로 Project, File, Folder 등록controllers: ProjectControllerproviders: ProjectServiceexports: ProjectService
- ConfigModule.forRoot
forFeature는 TypeOrmModule.forFeature입니다. 현재 File·Folder도 ProjectModule에서 등록하며, 별도 FileModule·FolderModule은 분리하지 않았습니다.
설정값과 저장소의 이름까지 맞추기
환경 파일을 읽었다는 사실과 값이 유효하다는 사실은 별개입니다. 앞의 parseInt(process.env.DATABASE_PORT, 10)은 앞부분의 정수를 읽기 때문에 합성 입력 '5432abc'를 넣어도 5432를 반환하고, 입력이 없으면 NaN이 됩니다. DB 접속을 시작하기 전에 포트를 십진 숫자 문자열 전체로 확인하고 정수 1~65535 범위를 검증하는 계약을 두면 이런 입력을 조기에 거절할 수 있습니다. 여기의 문자열은 설명용으로 만든 값이며 실제 환경 변수나 접속 정보를 읽은 결과가 아닙니다. ConfigModule에 사용자 정의 validate 함수 등을 연결하는 방식은 공식 설정 문서를 참고합니다. 현재 코드에는 그 검증 함수가 연결되어 있지 않습니다.
또 앞 코드의 envFilePath 조건식은 NODE_ENV가 정확히 'development'일 때만 .env.development를 고릅니다. 값이 없거나 'test'라면 조건식의 결과는 .env입니다. ConfigModule이 파일을 불러오는 것과, 읽은 값으로 import 시점의 조건을 바꾸는 것은 구별해야 합니다. 사용할 환경과 파일을 실행 전에 정하고 선택 경로와 필수 값의 검증 결과를 확인하되 접속 비밀값을 출력하지 않습니다.
여러 DB 연결로 확장한다면 연결 이름도 저장소 등록·주입 계약의 일부입니다. forRoot()에 name: 'editor'를 지정했다면 해당 File 저장소를 사용하는 forFeature([File], 'editor')와 @InjectRepository(File, 'editor')에서도 같은 이름을 지정해야 합니다. 이름을 생략하면 default 연결을 사용하므로 엔티티가 forRoot()에 있다는 사실만으로 이름이 다른 저장소 주입이 해결되지는 않습니다. 이 이름 규칙은 공식 TypeORM 통합 문서에 설명되어 있습니다. 앞 코드는 이름을 지정하지 않은 기본 연결을 사용하므로, 여기의 named 연결은 확장 시나리오입니다.
설정 경계를 확인하는 문제
문제 1. 포트 입력이 '5432abc', '0', '65536', '5432'일 때 parseInt() 결과가 있다는 이유만으로 모두 유효한 DB 포트라고 판단해도 되나요?
풀이. 안 됩니다. 십진 숫자 문자열 전체와 1~65535 범위를 계약으로 택하면 앞의 세 값은 각각 뒤의 문자, 범위 미달, 범위 초과로 거절하고 마지막 값만 5432로 수용합니다. parseInt('5432abc', 10)은 5432를 반환하므로 그 변환만으로 전체 문자열 검증을 대신하지 않습니다. 실제 접속 성공 여부는 추가로 DB 주소·가용성·인증 등을 검수해야 합니다.
문제 2. NODE_ENV를 지정하지 않았는데 .env.development가 자동 선택된다고 가정했습니다. 앞 코드의 조건식 결과는 무엇인가요?
풀이. undefined와 'development'는 같지 않으므로 .env가 선택됩니다. 파일 선택 조건과 실행 환경 지정이 일치하는지 먼저 확인합니다. 이 결과는 그 조건식의 평가이며 실제 PC의 환경 변수나 파일 내용을 조사한 결과가 아닙니다.
문제 3. forRoot()의 연결 이름은 'editor'인데 forFeature([File])와 @InjectRepository(File)에는 이름을 생략했습니다. 무엇을 맞춰야 하나요?
풀이. 두 곳이 기본 연결을 요청하므로 의도한 editor 연결과 어긋납니다. 같은 editor 이름을 forFeature()와 @InjectRepository()에 명시하고, 그 저장소가 필요한 모듈의 imports와 외부 사용 시 exports도 함께 확인합니다. ConfigModule의 isGlobal은 설정 서비스의 가시성에 관한 옵션으로, 모든 기능 모듈의 저장소를 자동으로 공유해 주는 옵션은 아닙니다.