백엔드 API 구현
온라인 코드 편집기의 데이터베이스를 연결하고 JWT 인증과 프로젝트·파일 관리 REST API를 NestJS로 구현합니다.
지난 절에서는 온라인 코드 에디터 및 실시간 협업 도구의 요구사항과 시스템 아키텍처를 설계했습니다.
이번 절에서는 그 설계를 바탕으로 NestJS 백엔드의 핵심인 API 구현을 시작합니다.
프로젝트 초기 설정부터 사용자 인증, 프로젝트/파일 관리를 위한 RESTful API 구현까지 순서대로 다룹니다.
실시간 협업 전에 인증·소유권·저장 경계를 Config→Entity→Service→Controller 순서로 안정화한다.
- 1Config
환경 변수와 DB 연결을 검증해 앱 시작 경계를 만든다.
- 2Entity
User·Project·File 관계와 소유권을 정의한다.
- 3Service
인증·저장·권한 규칙을 Controller 밖에 둔다.
- 4Controller
경로·DTO·상태 코드·실패 응답을 노출한다.
- 5Auth API
회원가입·로그인·JWT·보호 라우트를 검증한다.
- 6Project/File API
생성·목록·코드 저장과 소유권을 검증한다.
NestJS 프로젝트 초기 설정
먼저 NestJS CLI를 사용하여 새로운 프로젝트를 생성합니다.
# NestJS CLI가 없다면 설치
npm install -g @nestjs/cli
# 새 프로젝트 생성
nest new collaborative-code-editor-backend
# 프로젝트 디렉토리로 이동
cd collaborative-code-editor-backend
# 필요한 기본 패키지 설치
npm install @nestjs/typeorm typeorm pg bcryptjs @nestjs/jwt passport @nestjs/passport passport-jwt @types/passport-jwt @types/bcryptjs dotenv
# WebSocket 및 Socket.IO 관련 패키지는 나중에 설치
# npm install @nestjs/websockets @nestjs/platform-socket.io socket.io @types/socket.io설치된 주요 패키지 설명:
@nestjs/typeorm,typeorm,pg: TypeORM ORM을 사용하여 PostgreSQL 데이터베이스와 연동합니다.bcryptjs: 비밀번호 해싱을 위한 라이브러리입니다.@nestjs/jwt,passport,@nestjs/passport,passport-jwt: JWT(JSON Web Token) 기반 사용자 인증을 구현합니다.dotenv:.env파일에서 환경 변수를 로드합니다.
데이터베이스 설정
.env 파일을 생성하고 데이터베이스 연결 정보를 설정합니다.
# .env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=your_db_user
DATABASE_PASSWORD=your_db_password
DATABASE_NAME=code_editor_db
# JWT Secret (보안을 위해 강력하고 긴 문자열 사용)
JWT_SECRET=super_secret_key_please_change_this_in_productionsrc/app.module.ts 파일을 수정하여 TypeORM 모듈을 설정합니다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config'; // ConfigModule 임포트
@Module({
imports: [
// 환경 변수 로드
ConfigModule.forRoot({
isGlobal: true, // 전역적으로 사용 가능하도록 설정
envFilePath: process.env.NODE_ENV === 'development' ? '.env.development' : '.env', // 환경별 .env 파일 지정 가능
}),
// TypeORM 설정
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: [__dirname + '/**/*.entity{.ts,.js}'], // 엔티티 파일 경로 지정
synchronize: true, // 개발 단계에서만 true (자동으로 DB 스키마 생성/업데이트). 프로덕션에서는 migration 사용 권장
logging: ['query', 'error'], // 개발 시 쿼리 로그 확인
}),
],
controllers: [],
providers: [],
})
export class AppModule {}ConfigModule을 통해 .env 파일을 로드하여 환경 변수를 안전하게 사용할 수 있습니다.
사용자 인증 (Auth) API 구현
백엔드 API 구현에서는 요청이 들어오는 위치, 책임을 맡는 계층, 실패 응답으로 나가는 지점을 분리합니다.
Auth API는 로그인 폼 구현이 아니라 입력 검증, 비밀번호 해시, 토큰 발급, Guard 검증이 이어지는 보안 경계다.
- 1입력 검증
DTO 입력 검증 이메일과 비밀번호 형식을 제한한다.
- 2비밀번호 저장
hash 비밀번호 저장 평문 저장 없이 bcrypt 해시만 남긴다.
- 3JWT 발급
token JWT 발급 sub, email 같은 식별 정보만 payload에 둔다.
- 4토큰 복원
strategy 토큰 복원 요청마다 payload를 사용자 컨텍스트로 복원한다.
- 5보호 API
guard 보호 API req.user가 없으면 프로젝트 API에 들어오지 못한다.
| 구성 요소 | 책임 | 실패하면 | 보안 확인 |
|---|---|---|---|
| AuthService 가입/로그인 처리 | 사용자 생성, 비밀번호 비교, 토큰 발급 | 중복 이메일, 해시 누락, 잘못된 토큰 | 응답에 passwordHash가 없는가 |
| JwtStrategy 토큰 복원 | payload를 request user로 변환 | 보호 API가 익명 요청처럼 동작 | 만료/위조 토큰이 401인가 |
| AuthGuard 접근 제한 | 컨트롤러 진입 전 인증 강제 | 프로젝트/파일 API가 노출됨 | profile, projects가 토큰 없이는 실패하는가 |
사용자 관리를 위한 엔티티, DTO, 서비스, 컨트롤러 및 JWT 전략을 구현합니다.
User Entity
사용자 정보와 TypeORM 매핑을 정의합니다.
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { Project } from '../../project/entities/project.entity'; // Project 엔티티와 관계 설정 (미리 정의했다고 가정)
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true, nullable: false })
email: string;
@Column({ nullable: false })
password: string; // 해싱된 비밀번호 저장
@Column({ nullable: true })
nickname: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToMany(() => Project, project => project.owner)
projects: Project[];
}Auth DTOs
요청(Request) 데이터의 유효성 검사를 위한 DTO를 정의합니다.
npm install class-validator class-transformerimport { IsEmail, IsString, MinLength } from 'class-validator';
export class RegisterUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(6, { message: 'Password must be at least 6 characters long' })
password: string;
@IsString()
nickname?: string;
}import { IsEmail, IsString } from 'class-validator';
export class LoginUserDto {
@IsEmail()
email: string;
@IsString()
password: string;
}Auth Service
사용자 관련 비즈니스 로직(회원가입, 로그인)을 처리합니다.
import { Injectable, ConflictException, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { RegisterUserDto } from './dto/register-user.dto';
import { LoginUserDto } from './dto/login-user.dto';
import * as bcrypt from 'bcryptjs';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private jwtService: JwtService,
private configService: ConfigService,
) {}
async register(registerUserDto: RegisterUserDto): Promise<{ message: string }> {
const { email, password, nickname } = registerUserDto;
const existingUser = await this.usersRepository.findOne({ where: { email } });
if (existingUser) {
throw new ConflictException('Email already registered');
}
const hashedPassword = await bcrypt.hash(password, 10); // 비밀번호 해싱
const newUser = this.usersRepository.create({
email,
password: hashedPassword,
nickname,
});
await this.usersRepository.save(newUser);
return { message: 'User registered successfully' };
}
async login(loginUserDto: LoginUserDto): Promise<{ accessToken: string }> {
const { email, password } = loginUserDto;
const user = await this.usersRepository.findOne({ where: { email } });
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
// JWT 페이로드 (사용자 ID를 포함)
const payload = { sub: user.id, email: user.email };
const accessToken = this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET'),
expiresIn: '1h', // 토큰 만료 시간
});
return { accessToken };
}
// JWT 전략에서 사용될 사용자 조회 메서드
async validateUser(userId: string): Promise<User> {
return this.usersRepository.findOne({ where: { id: userId } });
}
}JWT Strategy (Passport)
요청 헤더의 JWT를 검증하고 사용자 정보를 추출합니다.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service'; // AuthService 주입
import { User } from './entities/user.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private configService: ConfigService,
private authService: AuthService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Bearer 토큰에서 JWT 추출
ignoreExpiration: false, // 만료된 토큰 거부
secretOrKey: configService.get<string>('JWT_SECRET'), // JWT Secret
});
}
async validate(payload: any): Promise<User> {
const user = await this.authService.validateUser(payload.sub);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}Auth Controller
API 엔드포인트를 정의합니다.
import { Controller, Post, Body, Get, UseGuards, Req } from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterUserDto } from './dto/register-user.dto';
import { LoginUserDto } from './dto/login-user.dto';
import { AuthGuard } from '@nestjs/passport'; // AuthGuard 임포트
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
async register(@Body() registerUserDto: RegisterUserDto) {
return this.authService.register(registerUserDto);
}
@Post('login')
async login(@Body() loginUserDto: LoginUserDto) {
return this.authService.login(loginUserDto);
}
@UseGuards(AuthGuard('jwt')) // JWT 가드 적용
@Get('profile')
getProfile(@Req() req) {
// req.user에는 JwtStrategy.validate()에서 반환된 사용자 정보가 담겨있음
const { password, ...result } = req.user; // 비밀번호는 제외하고 반환
return result;
}
}Auth Module
Auth 관련 모든 컴포넌트들을 묶습니다.
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forFeature([User]), // User 엔티티 등록
PassportModule,
JwtModule.registerAsync({ // 비동기적으로 JWT Secret 로드
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService, JwtModule], // 다른 모듈에서 AuthService와 JwtModule 사용 가능하도록 내보내기
})
export class AuthModule {}AppModule에 AuthModule 등록
src/app.module.ts에 AuthModule을 임포트합니다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module'; // AuthModule 임포트
@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: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
logging: ['query', 'error'],
}),
AuthModule, // AuthModule 등록
],
controllers: [],
providers: [],
})
export class AppModule {}아래 다이어그램은 회원가입과 로그인 이후, JWT가 보호 API 접근까지 전달되는 인증 파이프라인을 단계별로 보여줍니다.