본문으로 건너뛰기

안동민 개발노트

본문 시작

백엔드 API 구현

온라인 코드 편집기의 데이터베이스를 연결하고 JWT 인증과 프로젝트·파일 관리 REST API를 NestJS로 구현합니다.

지난 절에서는 온라인 코드 에디터 및 실시간 협업 도구의 요구사항과 시스템 아키텍처를 설계했습니다.

이번 절에서는 그 설계를 바탕으로 NestJS 백엔드의 핵심인 API 구현을 시작합니다.

프로젝트 초기 설정부터 사용자 인증, 프로젝트/파일 관리를 위한 RESTful 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_production

src/app.module.ts 파일을 수정하여 TypeORM 모듈을 설정합니다.

src/app.module.ts
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 구현에서는 요청이 들어오는 위치, 책임을 맡는 계층, 실패 응답으로 나가는 지점을 분리합니다.

환경 검증부터 principal까지 이어지는 인증 파이프라인

SEQUENCE · AUTHENTICATION

환경·DTO·비밀번호·JWT를 서로 다른 경계에서 검증한다

register와 login은 평문 비밀번호를 응답이나 토큰에 넣지 않는다. profile guard가 JWT를 검증한 뒤 userId와 email만 가진 principal을 전달한다.

환경 검증부터 principal까지 이어지는 인증 파이프라인 환경 검증, DTO 파이프, 컨트롤러, 비밀번호 경계, JWT 서명과 검증, 최소 principal이 순서대로 이어지는 인증 흐름이다. BOOT 환경 검증 secret · rounds HTTP ValidationPipe strict DTO ROUTES Auth Controller register · login SECRET Auth Service hash · compare TOKEN JWT Strategy sign · verify TRUSTED Principal userId · email
  1. 환경

    앱 시작 시 비밀키 길이와 해시 rounds를 검증합니다.

  2. DTO

    email·password 형식과 여분 필드를 요청 경계에서 거부합니다.

  3. 비밀번호

    register는 hash, login은 compare만 수행합니다.

  4. JWT

    검증된 sub와 email만 principal로 바꿉니다.

  5. 보호된 경로

    profile과 이후 자원 API가 같은 principal을 사용합니다.

화살표는 신뢰 또는 상태가 다음 경계로 이동하는 방향을 뜻합니다.

사용자 관리를 위한 엔티티, DTO, 서비스, 컨트롤러 및 JWT 전략을 구현합니다.

User Entity

사용자 정보와 TypeORM 매핑을 정의합니다.

src/auth/entities/user.entity.ts
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-transformer
src/auth/dto/register-user.dto.ts
import { 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;
}
src/auth/dto/login-user.dto.ts
import { IsEmail, IsString } from 'class-validator';

export class LoginUserDto {
  @IsEmail()
  email: string;

  @IsString()
  password: string;
}

Auth Service

사용자 관련 비즈니스 로직(회원가입, 로그인)을 처리합니다.

src/auth/auth.service.ts
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를 검증하고 사용자 정보를 추출합니다.

src/auth/jwt.strategy.ts
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 엔드포인트를 정의합니다.

src/auth/auth.controller.ts
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 관련 모든 컴포넌트들을 묶습니다.

src/auth/auth.module.ts
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.tsAuthModule을 임포트합니다.

src/app.module.ts (수정)
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 접근까지 전달되는 인증 파이프라인을 단계별로 보여줍니다.


이어서 보기