티스토리 뷰

728x90

NestJS 프로젝트 생성

controller

  • @Controller('posts')
    • /posts로 요청을 받겠다
  • @(Method이름)으로 메서드 표현

service

  • @Injectable() Annotation으로 Provider 등록

Request ➡️ Controller ➡️ Service ➡️ Controller ➡️ Response

 

RESTful API

  • REST 원칙을 잘 지킨 API
  • 자원은 무조건 URI로 표현하고 HTTP 메서드로 행위를 정의함
  • method에 따라 멱등성 특성이 다름
    • 멱등성 => POST, PUT
    • 멱등성 X => GET
메서드 목적 바디 멱등성 상태변경
GET 조회
POST 생성
PATCH 일부 수정
PUT 수정/생성
DELETE 삭제

 

HTTP URL 설계 가이드

  • 명사적 리소스 사용, 동사는 절대 금지!
    • X => getUsers , createUsers
    • O => user
  • 하위 리소스 표현
    • 부모-자식 관계가 명확할 때만 사용
    • ID 등 변동가능한 값은 path parameter로 표현
  • 복수형 사용 (권장) => 요거는 스타일에 따라 다를듯
  • 쿼리 파라미터
    • 요청의 조건만 표현.
    • page, sort
  • 액션
    • 행위를 URL에 나타내는것은 피하자 => 변동성을 주기 때문에
    • 의도를 명확하게 표현해야 할 때 사용 (예. /orders/1/cancel)

 

IoC 컨테이너

  • 의존성 주입이 돼야하는 객체들을 직접 생성해주고 운영해주는 서비스
  • @Injectable() Annotation으로 등록된 클래스는 모두 IoC 컨테이너에 등록됨!
@Controller('posts')
export class PostsController {
  // constructor 부분!
  constructor(private readonly postsService: PostsService) {}

 

pipe란?

  • Request ➡️ Guard ➡️ Interceptor ➡️ Pipe ➡️ Interceptor ➡️ ExceptionFilter ➡️ Controller
  • validationPipe 적용
    • @Param("id", ParseIntPipe) => 문자열을 정수로 변환해주는 파이프
    • 파라미터, 바디 등의 두번째 파라미터에 검증 클래스를 입력
    • 변환 실패 시 400 Bad Request 반환
    • ParseIntPipe, ParseBoolPipe, ParseUUIDPipe, ParseArrayPipe(콤마로 구분된 문자열 배열을 자바스크립트 배열로 변환), DefaultValuePipe, ParseEnumPipe
  • 파이프 중첩 또한 가능
    • @Param("id", new DefualtPipe(0), ParseIntPipe)

 

Class Validator

  • DTO 클래스를 만든다
  • 각 속성에 검증 데코레이터 추가
  • ValidationPipe를 통해 DTO 검증
  • ValidationPipe를 글로벌하게 적용해줘야 Annotation들이 적용됨!!!
    • transform / whitelist / forbidNonWhitelisted
    • @IsString, @IsNotEmpty, @IsInt, @Min
  • Custom Decorator
    • 함수를 반환함
pnpm i class-validator class-transformer
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true,
      forbidNonWhitelisted: true,
    }),
  );

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

 

export class CreatePostDto {
  @IsString({
    message: 'title은 문자열을 입력해주세요!',
  })
  @IsNotEmpty({
    message: 'title을 입력해주세요.',
  })
  @Length(3, 20, {
    message: 'title은 3자 이상, 20자 이하여야합니다.',
  })
  title: string;

  @IsString({
    message: 'content는 문자열을 입력해주세요!',
  })
  @IsNotEmpty({
    message: 'content를 입력해주세요.',
  })
  content: string;
}

 

Class Trnasformer

  • javascript 객체를 클래스 인스턴스로 변환하고 클래스 인스턴스를 javascript 객체로 변환하는 라이브러리
    • plainToInstance
    • instanceToPlain
  • @Type()
    • 중첩된 객체 dto 를 검증하려면 반드시 필요함
  • @Exclude, @Expose
    • JSON 변환시 노출 여부를 제어함
    • 기본이 @Expose임. 보통 특정 값만 Exclude를 닮
  • @Transform
    • 특정 필드의 값을 변형해서 가공할 수 있음

 

Mapped Types

  • PartialTypes
    • 모든 프로퍼티를 모두 optional화 해주는 타입
export class UpdatePostDto extends PartialType(CreatePostDto) {}

 

  • PickType
    • 일부 프로퍼티만 선택해서 타입을 만들고 싶을때 사용
export class UpdatePostDto extends PickType(CreatePostDto, ['title']) {}
  • OmitType
    • 특정 프로퍼티만 제외할 때 사용
  • IntersectionType
    • 두 개의 클래스를 결합 해주는 타입

 

-- 오후 --

 

TypeORM

  • 데코레이터 기반으로 선언적 데이터 모델링이 가능함
  • NestJS와 완벽히 통합가능하며 공식적으로 추천하는 ORM 중 하나
  • 세팅법
    • entities: 해당되는 테이블들을 연동해줌
    • synchronize: 개발용으로만 사용됨
  • Entity
    • 클래스와 테이블을 Entity로 매핑함. 컬럼 특성은 데코레이터를 사용해서 정의함
    • @Column() => options {.unique default, nullable }
    • @PrimaryGeneratedColumn()
    • @CreateDateColumn(), @UpdateDateColumn(), @DeleteDateColumn()
    • 관계관련 데코레이터 => @OneToOne(), @OneToMany, @JoinColumn()...
    • 기타 => @Index, @Unique, @VersionColumn(), @Check(), @Exclude(), @Include
  • Repository
    • SQL 없이 조작 가능하도록 해주는 객체
    • @InjectRepository()를 사용해서 사용할 수 있음
    • 메서드
      • find()
      • findOne() / findOneBy()
      • save(), insert() => id 없는 raw insert로 save보다 조금 빠름
      • update(), delete(), remove()

 

QueryBuilder

  • SQL을 Typescript 스타일로 작성 할 수 있도록 해주는 기능
  • 조인, 필터링, 서브쿼리, 페이징 등의 기능을 가능하게 함
  • select(), where(), andWhere(), orWhere(), join(), leftJoinAndSelect(), orderBy(), skip()/take(), getOne(), getMany(), getRawMany()
  • createQueryBuilder(alias_name)~~~

 

Repository vs QueryBuilder

  • Repository를 사용할 수 없는 상황에 QueryBuilder 사용!

 

Docker

  • HostOS 공유
  • Dockerfile: 이미지를 어떻게 만들것인가? 이미지 만드는 레시피
  • Image: 컨테이너 찍어내는 틀. Dockerfile을 하나의 이미지로 저장해서 무한으로 사용 가능
  • Container: 이미지가 틀이면 컨테이너는 틀로 찍어낸 붕어빵
  • Docker Engine: Docker를 실행시키는 엔진
  • Registry: 저장소, Docker Hub
  • ENV, ARG, VOLUME...

Docker Compose

  • Dockerfile이 너무 많아지면 각각 관리하는건 불가능함
    • FE, BE, Database 모두 도커로 관리하면 CLI만으로 힘듦
  • 이러한 툴로 일괄적으로 여러 컨테이너를 관리할 수 있음
  • 비슷한 툴로는 Kubernates, Docker Swarm
  • 얘는 제한적인 기능이라 개발 용도로만 사용함

docker-compose.yml

services:
  postgres:
    image: postgres:16

    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres

    ports:
      - '3001:5432'

    # 왼쪽이 호스트
    # 이미지가 삭제되어도 데이터가 유지될 수 있게 해줌
    volumes:
      - ./postgres-data:/var/lib/postgresql/data

 

 

TypeOrm 실습

pnpm i @nestjs/typeorm typeorm pg

import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
} from 'typeorm';

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  content: string;

  @CreateDateColumn()
  createdDate: Date;
}

 

-------------- 2일차-------------- 

로깅

  • nestjs의 기본 Logger
    • import { Logger } from '@nestjs/common'
    • 단점: 콘솔 출력만 됨. 파일 저장, 포맷지정, 외부 로그 전송 불가
  • Winston
    • nodeJS에서 가장 많이 사용되는 로깅 라이브러리
    • 로그 레벨, 로그 포맷, Transport 지정 가능
    • AppModule에 임포트해서 세팅
    • @Inject 데코레이터로 Logger를 주입받아서 서비스단에서 사용
    • pnpm i winston nest-winston
import { WinstonModuleOptions } from 'nest-winston';
import * as winston from 'winston';

export const winstonConfig: WinstonModuleOptions = {
  level: 'verbose', // 가장 낮은 레벨의 로깅
  transports: [
    // 콘솔 출력
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.colorize(), // 레벨별로 색상을 다르게 하겠다
        winston.format.printf(({ level, message, timestamp, context }) => {
          return `[${timestamp}] ${level} [${context} || 'App] ${message}`;
        }),
      ),
    }),
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
    }),
    new winston.transports.File({
      filename: 'logs/combined.log',
    }),
  ],
};

 

 @Inject(WINSTON_MODULE_NEST_PROVIDER)
    private readonly logger: LoggerService,

 

Interceptor란?

  • 컨트롤러에서 요청을 처리하기 전이나 응답을 반환하기 전에 가로채서 추가 작업을 수행할 수 있는 컴포넌트
  • 서버 전반적으로 적용하거나 (app module에 적용), 컨트롤러 혹은 메서드 단위로 적용이 가능함 (@UseInterceiptors 데코레이터)
import {
  CallHandler,
  ExecutionContext,
  Inject,
  Injectable,
  LoggerService,
  NestInterceptor,
} from '@nestjs/common';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggerInterceptor implements NestInterceptor {
  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    private readonly logger: LoggerService,
  ) {}

  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> {
    const req = context.switchToHttp().getRequest();
    const { methods, originalUrl } = req;

    const now = Date.now();

    return next.handle().pipe(
      // 가장 비파괴적인 작업을 할 때 tap 사용
      tap(() => {
        const res = context.switchToHttp().getResponse();
        const statusCode = res.statusCode;

        this.logger.log(
          `[${methods}] ${originalUrl} -> ${statusCode} + ${Date.now() - now}`,
        );
      }),
    );
  }
}

 

 

Exception Filter

  • @Catch 데코레이터에 적용하고 싶은 에러를 입력함
  • implements ExceptionFilter
  • 서버 전반적으로 적용하거나 (app module에 useGlobalFilters 적용), 컨트롤러 혹은 메서드 단위로 적용이 가능함 (@UseFilters 데코레이터)
  • 글로벌하게 에러의 형태를 바꿔주는 필터!
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  Inject,
  LoggerService,
} from '@nestjs/common';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';

@Catch(HttpException)
export class ErrorExceptionFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    private readonly logger: LoggerService,
  ) {}

  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest<Request>();

    const status = exception.getStatus();
    const message = exception.getResponse();

    this.logger.error('에러 발생!');

    response.status(status).json({
      statusCode: status,
      message,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

 

인증과 인가

Authentication vs Authorization

  • Authentication: 사용자를 확인하는 과정
    • 비밀번호, OTP, 인증서 등
  • Authorization: 사용자가 무엇을 할 수 있는지 결정하는 권한 관리
    • RBAC : Role Based Access Control, 사용자에게 역할을 부여하고 역할에 따라 권한 설정 (admin, user, gust..)
    • PBAC (Permission Based): 읽기/쓰기/삭제 등의 권한을 정의
    • ABAC (Attribute Based): 리소스, 환경, 사용자 속성을 기반으로 권한 정의

 

인증 방법

  • 세션 기반
    • 리소스 요청 시 Client, Server, Database가 함께 작동해야 함
  • 토큰 기반
    • 리소스 요청 시 서버에서 토큰 검증을 한 후 바로 응답을 보내줄 수 있음 (Database 관여 X)

 

JWT Token (JSON Web Token)

  • <Header>.<Payload>.<Signature>
  • Header: 알고리즘 + 타입
  • Payload: 사용자 정보
  • Signature: Header + Paylaod 해서 secretKey 사용해서 암호화
  • Basic 토큰: Basic {username:password} 를 base64 인코딩한 값, 로그인 할 때 전송
  • Bearer 토큰: Bearer {jwt토큰}

 

암호화

  • 플레인 비밀번호
  • 해싱
    • 비밀번호를 복호화되지 않는 알고리즘으로 해싱해서 디비에 저장. 
    • 디비가 털려도 오리지널 비밀번호를 알 수 없음!
  • Dictionary Attack
    • 어떻게 해싱된 비밀번호가 맞는지를 알 수 있을까?
    • salt 값을 비밀번호에 합쳐서 함께 해싱해서 미리 Dictionary를 만듦. 공격 방어
  • Bcrypt, scrypt, Argon2 알고리즘
    • 고의적으로 느리게 실행되도록 설계됨
    • Round를 증가시켜 원하는만큼 느리게 실행 가능함 (bcrypt의 경우)
    • Salt를 알아내더라도 알고리즘이 느려서 Dictionary 생성이 너무 오래걸림

회원가입

import {
  BadRequestException,
  Injectable,
  InternalServerErrorException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from 'src/users/entities/user.entity';
import { DataSource, Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { RegisterDto } from './dto/register.dto';
import { UserProfileEntity } from 'src/users/entities/user-profile.entity';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>,
    @InjectRepository(UserProfileEntity)
    private readonly userProfileRepository: Repository<UserProfileEntity>,
    private readonly dataSource: DataSource,
  ) {}

  hashPassword(password: string): Promise<string> {
    return bcrypt.hash(password, 10);
  }

  comparePasswordAndHash(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
  }

  async register(registerDto: RegisterDto) {
    const hashPassword = await this.hashPassword(registerDto.password);

    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction(); // 밑의 과정들이 transaction 처리가 가능함

    try {
      const user = this.userRepository.create({
        ...registerDto,
        password: hashPassword,
      });
      await queryRunner.manager.save(user);

      const profile = this.userProfileRepository.create({
        user: user,
        bio: registerDto.bio,
      });

      // repository의 save를 사용하면 transaction 적용이 안됨.
      await queryRunner.manager.save(profile);

      await queryRunner.commitTransaction(); // startTransaction, commitTransaction 사이의 과정이 데이터베이스에 적용이 됨

      return await this.userRepository.findOneBy({ email: registerDto.email });
    } catch (e) {
      await queryRunner.rollbackTransaction();

      if (e.code === '23505') {
        if (e.detail.includes('(email)=')) {
          throw new BadRequestException('이미 가입한 이메일 입니다!');
        }
        throw new BadRequestException('중복 에러가 발생했습니다!');
      }

      throw new InternalServerErrorException();
    } finally {
      await queryRunner.release();
    }
  }
}

 

토큰을 이용한 로그인

 

Guard

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';

@Injectable()
export class AccessTokenGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const authHeader = request.headers['authorization'];

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      throw new UnauthorizedException('AccessToken이 없습니다');
    }

    const token = authHeader.replace('Bearer ', '').trim();

    try {
      const payload = this.jwtService.verify(token);

      request['user'] = payload;

      return true;
    } catch (e) {
      throw new UnauthorizedException('유효하지 않은 토큰입니다!');
    }
  }
}

 

 

CORS

  • 브라우저는 다른 출처로의 요청을 자동으로 제한함
  • 브라우저에서 제한하는 사항이지만 해제 불가능하므로, 서버에서 처리해줘야함!
  • main.ts에 enableCors 세팅해주기
app.enableCors({
    origin: ['https://www.google.com'],
  });

 

파일 업로드

  • 전통적 FormData 방식
    • 입력 필드와 업로드 파일을 한번에 서버로 전달
    • 파일이 클수록 느리다고 느껴짐
  • 선업로드 방식
    • 파일 선택과 동시에 임시 폴더(서버측)로 업로드를 진행함
    • 업로드 경로만 받아서 submit시 전달
    • 사용성 굿
    • AWS S3 presigned url
  • multipart/form-data
    • 폼 데이터를 여러 파트로 나누어 전송하는 HTTP 콘텐츠 타입
  • 서버에서 전달받는 법: NestJS + Multer
    • FileInterceptor만 사용하면 바로 파일을 받을 수 있음
@Post('register')
  @UseInterceptors(
    FileInterceptor('profileImage', {
      storage: diskStorage({
        destination: join(process.cwd(), 'uploads', 'profileImage'),
        filename: (req, file, cb) => {
          // 바로 로컬 저장소에 파일이 쓰여짐
          const uuid = v4();

          const ext = extname(file.originalname);
          const fileName = uuid + ext;

          cb(null, fileName);
        },
      }),
      fileFilter: (req, file, cb) => {
        if (file.mimetype.match(/\/(jpg|jpeg|png)$/)) {
          cb(null, true);
        } else {
          cb(new BadRequestException('지원하지 않는 형식입니다.'), false);
        }
      },
    }),
  )
  async register(
    @UploadedFile() file: Express.Multer.File,
    @Body() registerDto: RegisterDto,
  ) {
    console.log(file);

    return this.authService.register(registerDto);
  }

 

 

스웨거

  • nestJS의 데코레이터와 스웨거가 자동화가 굉장히 잘됨
  • DocumentBuilder로 요소를 설정만 해주면 Controller의 스펙을 기반으로 기본 다큐멘테이션이 잘 생성됨
  • @ApiTags: API를 그룹으로 묶을 때 사용됨
  • @ApiOperation: API의 설명을 추가하는데 사용됨, summary와 description 추가할때
  • @ApiResponse: API 응답값에 대한 정보를 추가할 때 사용
    • 상태코드별로 미리 생성된 유형의 데코레이터들이 존재함
  • @ApiProperty: 프로퍼티를 스웨거의 다큐멘테이션에 노출하고 싶을때 사용함. description / example..
    • 기본적으로 모두 hidden
  • @ApiBearerAuth: 스웨거에서 테스트할때 JWT 토큰을 헤더에 포함시키는 역할

 

AWS 기본 컴포넌트

  • EC2
    • 월세주는것처럼 사용자에게 임대해주자
    • AWS에서 제공하는 클라우드 기반 가상 서버
  • RDS: EC2인데 관계형 데이터베이스에 최적화된 서버
  • VPC (Virtual Private Cloud)
    • aws 계정을 만들면 자동으로 생기는 것. 주거공간을 만든다
    • 이 안에다 리소스를 배포할 수 있게 됨.
  • ELB (Elastic Load Balancer): 트래픽을 여러 대상 (EC2인스턴스)에 자동 분산시켜 애플리케이션의 가용성과 확장성을 향상시키는 서비스
  • Route 53: 클라우드 DNS 웹 서비스
  • EB (Elastic Beansatlk): 배포를 단순화하는 관리형 서비스. 코드만 업로드하면 자동으로 인프로 프로비저닝, 로드 밸런싱, 스케일링 등을 처리
  • Lightsail: EB보다 더 간결. 소규모 프로젝트를 빠르게 시작할 수 있도록 도와주는 서비스. 가상 서버, 스토리지, 데이터 전송 등을 하나의 패키지로 제공
  • IAM (Identity Access Management): 사용자 및 그룹의 권한을 관리
  • S3 (Simple Storage Service): 확장 가능한 객체 스토리지. 대용량 데이터를 안전하고 저렴하게 저장하고 검색이 가능함
728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함