티스토리 뷰
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
링크
TAG
- 이코테
- 이것이 취업을 위한 코딩테스트다
- 기초
- springboot
- redux
- css
- JavaScript
- React
- reactjs
- programmers
- 노마드코더
- TypeScript
- 자바스크립트
- level3
- 면접을 위한 CS 전공지식 노트
- 프로그래머스
- 소프티어
- html
- React.FC
- 이것이코딩테스트다
- 파이썬
- level1
- CS
- 상태관리
- dfs
- nomadcoder
- axios
- Hook
- 이진탐색
- CORS
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함