Nodejs

 

 

이 포스트는 NestJS, PostgreSQL, Docker를 활용한 회원가입 시스템 구축 과정을 실습 중심으로 설명합니다. 실제 강의에서 제공된 코드를 기반으로, 각각의 파일이 어떤 역할을 하는지 기술 블로그 스타일로 정리했습니다.

 

 1. 환경 구성 및 의존성

.dockerignore

NestJS 환경에서 Docker 이미지 생성 시 불필요한 파일을 제외하기 위한 설정입니다.

node_modules
dist
.git
.gitignore
Dockerfile
docker-compose.yml
*.log
*.env

설치 라이브러리

pnpm add @nestjs/config @nestjs/jwt @nestjs/typeorm @nestjs bcrypt class-transformer class-validator joi pg typeorm

이 라이브러리들은 NestJS 앱 구성, 유효성 검사, DB 연동, 암호화 등에 사용됩니다.

 

 

 

 

 2. Docker Compose 구성

docker-compose.yml

NestJS 앱과 PostgreSQL을 함께 실행하기 위한 설정입니다.

services:
  user:
    build:
      context: .
      dockerfile: ./apps/user/Dockerfile
      target: development
    command: pnpm run start:dev user
    depends_on:
      postgres_user:
        condition: service_healthy
    env_file:
      - ./apps/user/.env    
    ports:
      - '3001:3000'
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules

  postgres_user:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    ports:
      - '6001:5432'
    volumes:
      - ./postgres/user:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 5s

 

 

 3. 앱 모듈 구성 (AppModule)

app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({
       isGlobal: true,
       validationSchema:Joi.object({
          HTTP_PORT: Joi.number().required(),
          DB_URL: Joi.string().required(),
       }),
    }),
    TypeOrmModule.forRootAsync({
       useFactory: (configService: ConfigService) => ({
          type: 'postgres',
          url: configService.getOrThrow('DB_URL'),          
          autoLoadEntities: true,
          synchronize: true
       }),
       inject: [ConfigService],
    }),
    UserModule,
    AuthModule,
  ],
})
export class AppModule {}

 

 

 4. 유저 Entity 설계

user.entity.ts
@Entity()
export class User {
    @PrimaryGeneratedColumn('uuid')
    id: string;

    @Column({ unique: true })
    email: string;

    @Column()
    name: string;

    @Column()
    age: number;

    @Column()
    profile: string;

    @Column({ select: false })
    password: string;

    @CreateDateColumn()
    createdAt: Date;

    @UpdateDateColumn()
    updatedAt: Date;

    @VersionColumn()
    version: number;
}

 

 

 5. DTO 유효성 검증

create-user.dto.ts
export class CreateUserDto{
    @IsEmail()
    @IsNotEmpty()
    email: string;

    @IsString()
    @IsNotEmpty()
    password: string;

    @IsString()
    @IsNotEmpty()
    name: string;

    @IsNumber()
    @IsNotEmpty()
    age: number;

    @IsString()
    @IsNotEmpty()
    profile: string;
}

 

 

 6. 사용자 서비스 (UserService)

user.service.ts
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
 ){}

  async getUserById(userId: string){
    const user = await this.userRepository.findOneBy({id: userId});
    if(!user){
      throw new BadRequestException('존재하지 않는 사용자입니다!');
    }
    return user;
  }

  async create(createUserDto: CreateUserDto){
    const {email, password} = createUserDto;

    const user = await this.userRepository.findOne({ where:{ email } });
    if(user){
      throw new BadRequestException('이미 가입한 이메일 입니다!')
    }

    const hash = await bcrypt.hash(password, 10);

    await this.userRepository.save({
      ...createUserDto,
      email,
      password: hash,
    });

    return this.userRepository.findOne({ where:{ email } });
  }
}

 

 

 7. 인증 서비스 (AuthService)

auth.service.ts
@Injectable()
export class AuthService {
  constructor(private readonly userService: UserService) {}

  async register(rawToken: string, registerDto: any) {
    const { email, password } = this.parseBasicToken(rawToken);
    return this.userService.create({
      ...registerDto,
      email,
      password,
    });
  }

  parseBasicToken(rawToken: string) {
    const basicSplit = rawToken.split(' ');
    if (basicSplit.length !== 2) {
      throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
    }

    const [basic, token] = basicSplit;
    if (basic.toLowerCase() !== 'basic') {
      throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
    }

    const decoded = Buffer.from(token, 'base64').toString('utf-8');
    const tokenSplit = decoded.split(':');
    if (tokenSplit.length !== 2) {
      throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
    }

    const [email, password] = tokenSplit;
    return { email, password };
  }
}

 

 

 8. 사용자 정의 데코레이터

authorization.decorator.ts
export const Authorization = createParamDecorator(
  (data: any, context: ExecutionContext) => {
    const req = context.switchToHttp().getRequest();
    return req.headers['authorization'];
  }
);

 

 

 

 9. Webpack 설정

webpack.config.js
module.exports = function(options){
  return {
    ...options,
    devtool: 'source-map',
  }
}

 

 

 

 

 

about author

PHRASE

Level 60  라이트

결혼 기념일과 아내의 생일등 아내에게 특별한 날들을 잊지 말고 함께 축하하고 기뻐하라. -남편십계명-

댓글 ( 0)

댓글 남기기

작성