본문으로 건너뛰기

안동민 개발노트

본문 시작

백엔드 API 구현

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

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

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

프로젝트 초기 설정부터 사용자 인증, 프로젝트/파일 관리를 위한 RESTful API 구현까지 순서대로 다룹니다.

협업 기능은 설정과 데이터 모델을 거쳐 REST API가 된다

실시간 협업 전에 인증·소유권·저장 경계를 Config→Entity→Service→Controller 순서로 안정화한다.

  1. 1
    Config

    환경 변수와 DB 연결을 검증해 앱 시작 경계를 만든다.

  2. 2
    Entity

    User·Project·File 관계와 소유권을 정의한다.

  3. 3
    Service

    인증·저장·권한 규칙을 Controller 밖에 둔다.

  4. 4
    Controller

    경로·DTO·상태 코드·실패 응답을 노출한다.

  5. 5
    Auth API

    회원가입·로그인·JWT·보호 라우트를 검증한다.

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

회원가입, 로그인, 보호 API는 하나의 인증 파이프라인이다

Auth API는 로그인 폼 구현이 아니라 입력 검증, 비밀번호 해시, 토큰 발급, Guard 검증이 이어지는 보안 경계다.

  1. 1
    입력 검증

    DTO 입력 검증 이메일과 비밀번호 형식을 제한한다.

  2. 2
    비밀번호 저장

    hash 비밀번호 저장 평문 저장 없이 bcrypt 해시만 남긴다.

  3. 3
    JWT 발급

    token JWT 발급 sub, email 같은 식별 정보만 payload에 둔다.

  4. 4
    토큰 복원

    strategy 토큰 복원 요청마다 payload를 사용자 컨텍스트로 복원한다.

  5. 5
    보호 API

    guard 보호 API req.user가 없으면 프로젝트 API에 들어오지 못한다.

구성 요소책임실패하면보안 확인
AuthService 가입/로그인 처리사용자 생성, 비밀번호 비교, 토큰 발급중복 이메일, 해시 누락, 잘못된 토큰응답에 passwordHash가 없는가
JwtStrategy 토큰 복원payload를 request user로 변환보호 API가 익명 요청처럼 동작만료/위조 토큰이 401인가
AuthGuard 접근 제한컨트롤러 진입 전 인증 강제프로젝트/파일 API가 노출됨profile, projects가 토큰 없이는 실패하는가

사용자 관리를 위한 엔티티, 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 접근까지 전달되는 인증 파이프라인을 단계별로 보여줍니다.


이어서 보기