์น๊ฐ๋ฐ์ ์์ด ๊ฐ๋ฐ ์ฌ์ดํด์ ์ ๋ฐ์ ์ธ ํ๋ฆ์ ๊ฒฝํํ๊ธฐ ์ํด ์งํํ ๊ฐ์ธ ํ๋ก์ ํธ ์ ๋๋ค.
๊ธฐํ, ๋์์ธ๋ถํฐ ์ด์๊ณผ ๋ฐฐํฌ๊น์ง ์์ด ์ ์ฒด์ ์ธ ๊ฐ๋ฐ๊ธฐ๊ฐ์ 2022.02 ~ 2022.04 ์ ๋๋ค.
๋ชจ๋ฐ์ผ์์๋ ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋๋ก ๋ชจ๋ฐ์ผ ๋ฐ์ํ ๋์์ธ์ ๊ตฌํํ์ต๋๋ค.
์ค์ ์๋น์ค๋ ํฌ๋ก๋๋ฒจ๋กํผ ์์ ์ฌ์ฉ์ด ๊ฐ๋ฅํฉ๋๋ค.
- Visual Studio Code
- DBeaver
- Mysql Workbench
- Figma
- Notion
- Slack
- Typescript
- Nestjs
- Mysql + TypeORM
- AWS EC2
- AWS S3
- AWS ACM
- AWS Route53
- AWS ELB(ALB)
- AWS RDS
- AWS ECR
- Docker
- Passport.js
- JWT
- Typescript
- React
- Recoil
- Styled-components
- AWS S3
- AWS CloudFront
- AWS Route53
- CI/CD : Github Actions
- Markdown Editor/Viewer : Toast UI Editor
- Auth : ์ธ์ฆ(๋ก๊ทธ์ธ, ํ์๊ฐ์ , ํ ํฐ ๋ฑ) ๊ด๋ จ ๋ชจ๋
- Boards : ๊ฒ์๊ธ(์์ฑ, ์ญ์ ๋ฑ) ๊ด๋ จ ๋ชจ๋
- User: ์ฌ์ฉ์(ํ๋กํ, ํ๋๋ด์ญ ๋ฑ) ๊ด๋ จ ๋ชจ๋
graph TD;
APP-->Auth;
APP-->Boards;
APP-->Users;
Auth-->Email
Auth-->Util
Boards-->Util
Boards-->AWS
Boards-->Comments;
Users-->AWS
AWS ACM์์ ๋ฐ๊ธ๋ฐ์ SSL์ธ์ฆ์๋ฅผ ์ฌ์ฉํ์ฌ ELB(ALB)๋ฅผ ํตํด HTTPS๋ฅผ ์ ์ฉํ์ต๋๋ค.
EC2์์๋ ECR์ ์ ๋ก๋๋ ์ด๋ฏธ์ง๋ฅผ ๋ฐ์์ค๊ณ Docker Image๋ฅผ ์คํํ์ฌ ๋ฐฐํฌํฉ๋๋ค.
S3์ ๋ณ๊ฒฝ์ฌํญ์ด ๋ฐ๋ก ์ ์ฉ๋ ์ ์๋๋ก CloudFront์์ Invalidate Cache๋ฅผ ์ ์ฉํ์ต๋๋ค.
๊ฐ API์ Controller์ Service ๋ ์ด์ด์ ๋ํ 90๊ฐ์ ์ ๋ํ ์คํธ ์ผ์ด์ค๋ฅผ ์์ฑํ์ต๋๋ค.
์์ ๋ ์ด์ด์ ์์กดํ๋ ๋ก์ง์ ๊ฒฝ์ฐ ํ ์คํธ์ ๊ฒฉ๋ฆฌ๋ฅผ ์ํ์ฌ Mocking ์ฒ๋ฆฌํ์ต๋๋ค.
ํ ์คํธ์๊ฐ์ ์ค๊ธฐ์ด ์ํ์ฌ ์ค์ DB๊ฐ ์๋ ์ธ-๋ฉ๋ชจ๋ฆฌ DB์ธ SQLite๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
Restful API์ ๋ฌธ์๋ฅผ ์๋์ผ๋ก ๊ตฌ์ฑํด์ฃผ๋ Swagger ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํด์ ์์ฑํ์ต๋๋ค.
API ๋ฌธ์ URL : Hello Developer API Docs.
์๋ฒ์์ ์น๋ช ์ ์ธ 500๋์ HTTP ์๋ฌ๊ฐ ๋ฐ์ํ๊ฒฝ์ฐ์ ๋ก๊น ํ๋๋ก ์ฒ๋ฆฌํ์ต๋๋ค.
์๋ํฐ๋ฅผ ํตํด ์ด๋ฏธ์ง ์ ๋ก๋ ๋ฐ DB์ ์ ์ฅ์ ์๋์ ๊ฐ์ ์ํฉ์ด ๋ฐ์ํ์ต๋๋ค.
- ๋งํฌ๋ค์ด ์๋ํฐ์์ ์ด๋ฏธ์ง ์ฝ์ ์ ์ธ์ฝ๋ฉ๋์ด ๋ณธ๋ฌธ์ ๊ธธ์ด๊ฐ ๊ธธ์ด์ง๋ ์ํฉ
- ์ฝ 4kb์ ์ด๋ฏธ์ง ์ฝ์ ์ ์ฝ 5100์์ ๋ฌธ์์ด์ด ์ฝ์
- ๋ณธ๋ฌธ์ ๊ธธ์ด๊ฐ ๋์ด๋ ์๋ก DB์์ ์ฐจ์งํ๋ ๊ณต๊ฐ์ด ์ปค์ง๊ฒ๋๋ฉฐ, ์ฝ์ /์ญ์ ๋ฑ ๋ฐ์ดํฐ์ ํฌ๊ธฐ๊ฐ ๋์ด๋ ์๋ก ๋ ๋ง์์์ ๋คํธ์ํฌ ํธ๋ํฝ์ ์๊ตฌ
์ด๋ฏธ์ง ์ ๋ก๋ ์ด๋ฒคํธ๋ฅผ ์ปค์คํ ํ์ฌ AWS S3์ ์ ๋ก๋ ๋ฐ ์ด๋ฏธ์ง URL์ ๋ฐํํ์ฌ ์ ๋ก๋๋ฅผ ์ต์ ํ ํ์ต๋๋ค.
ํด๊ฒฐ๊ณผ์ ์ Tistory ๋ธ๋ก๊ทธ์ ์ ๋ฆฌํ์ต๋๋ค.
๊ธฐ์กด ๊ฒ์๊ธ์ ์์ฑํ๋ DB Column ์์ฑ์ UUID ํ์์ ๋๋คํ ๊ฒ์๊ธ ID๊ฐ ์์ฑ๋๋ ํ์
ํ์ง๋ง ์ด๋ฏธ์ง ์
๋ก๋์์ ๊ฒ์๊ธ์ด DB์ ์ ์ฅ๋๋ ์์ ๋ณด๋ค ์ด์ ์ ์
๋ก๋ ์ด๋ฒคํธ๊ฐ ๋ฐ์
์์ ๊ฐ์ ๋ฌธ์ ์ ์ ์๋์ ๊ฐ์ด ๋ฐฉ๋ฒ์ผ๋ก ๊ฐ์ ํ์์ต๋๋ค.
- ๊ฒ์๊ธ ์์ฑ์ ํด๋ผ์ด์ธํธ์์ ์์ ๊ฒ์๊ธ ID ์์ฑ
- ์์ ๊ฒ์๊ธ ID๋ฅผ ๊ธฐ์ค์ผ๋ก S3์ /temp-board-id/image.png ํ์์ผ๋ก ์ ๋ก๋
- ๊ฒ์๊ธ์ด ์ ์ฅ๋๋ ์์ ์ ๊ฒ์๊ธ DTO์ UUID ๋ฌธ์์ด์ ID๋ก ์ถ๊ฐ
- ๊ฒ์๊ธ์ด ์ ์ฅ๋๋ ๋์์ S3์ ์กด์ฌํ๋ temp-board-id๋ฅผ ์ค์ ๊ฒ์๊ธ ID๋ก ์นํ
sequenceDiagram
participant Client
participant Server
participant AWS
Client->>Server: Image Upload Req
Server->>AWS: Image Upload in S3
Server->>Client: Return Image URL
Client->>Server: Save Board Req
Server->>AWS: Change Folder Name
nodemailer + gmail์ ์ฌ์ฉํ์ฌ ์ด๋ฉ์ผ ์ธ์ฆ์ ๊ตฌํ์ค ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
- 2022.05.30 ์ดํ OAuth2.0 ๋ฏธ์ฌ์ฉ์ Gmail API ์ฌ์ฉ ๋ถ๊ฐ
- ๊ณต์๋ฌธ์์์๋ OAuth ์ฌ์ฉ๋ฐฉ๋ฒ๊ณผ ๊ด๋ จ๋ ์์ธํ ๋ฐฉ๋ฒ์ด ๋์์์ง ์์์
์ ์ ์ฝ์ฌํญ์ผ๋ก ์ธํด Gmail API์ OAuth์ nodemailer๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์ฆ๋ฉ์ผ ๋ฐ์ก๊ธฐ๋ฅ์ ๊ตฌํํ์ต๋๋ค.
ํด๊ฒฐ๊ณผ์ ์ Tistory ๋ธ๋ก๊ทธ์ ์ ๋ฆฌํ์ต๋๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก Access Token์ ๊ฒฝ์ฐ localStorage๋ฑ ํด๋ผ์ด์ธํธ(๋ธ๋ผ์ฐ์ )์ ์ ์ฅ๊ณต๊ฐ์์ ๋ณด๊ดํฉ๋๋ค.
์ด์ ๋ฐ๋ผ์ ์๋ช
์ด ๊ธธ๋ฉด ํ์ทจ๋นํ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
JWT Refresh Token์ ํ์ฉํ์ฌ ์ ์ ์ ์ฌ์ฉ๊ฒฝํ์ด ๋ถํธํ์ง ์๋๋ก ๊ฐ์ ํ์ต๋๋ค.
- ํ ํฐ์ ์ ํจ์๊ฐ์ ๊ฒฝ์ฐ Access Token์ 15๋ถ, Refresh Token์ 10์ผ๋ก ์ง์
- Refresh Token์ httpOnly, Secure ์ต์ ์ผ๋ก ์ฟ ํค์ ์ ์ฅ
- ์๋ฒ์์ 401 Unauthorized ์๋ฌ ๋ฐํ์ Refresh Token์ ํ์ฉํ ์ฌ์์ฒญ ๋ฐ Access Token ์ฌ๋ฐ๊ธ
res.cookie('refreshToken', refreshToken, { httpOnly: true, path: '/', secure: true });๊ฒ์๊ธ ์์ธ๋ณด๊ธฐ๋ฅผ ๋๋ฅด๊ฒ๋๋ฉด ์กฐํ์๋ฅผ ์ฆ๊ฐํ๊ธฐ ์ํด API๋ฅผ ํธ์ถํ๊ฒ ๋ฉ๋๋ค.
์ด๋ ์ด๋ฏธ ์กฐํํ ๊ฒ์๋ฌผ๋ ๊ณ์ API ํธ์ถ์ด ๋ค์ด์ค๊ฒ ๋ฉ๋๋ค.
์ฌ์ฉ์๊ฐ ์
์์ ์ผ๋ก ์๋ก๊ณ ์นจ ํ
๋ฌ๋ฅผ ํ๋๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํด ์ฟ ํค๋ฅผ ์ค์ ํ์ฌ ํด๊ฒฐํ์ต๋๋ค.
- ๊ฒ์๊ธ ์์ธ๋ณด๊ธฐ ํ์ด์ง ์ ์์ 24์๊ฐ๋์ ์ ์ง๋๋ ์ฟ ํค ์์ฑ
- ๊ฒ์๊ธ ์์ธ๋ณด๊ธฐ ํ์ด์ง ์ ์์ ์ฟ ํค ์ฌ๋ถ์ ๋ฐ๋ผ ์กฐํ์ ํธ์ถ์ฌ๋ถ ๊ฒฐ์
๊ฒ์๊ธ ๋ชฉ๋ก ๋๋ ๊ฒ์๊ธฐ๋ฅ ์ฌ์ฉ์ DB์ ์กด์ฌํ๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํ๋ฒ์ ์๋ต๋ฐ์ต๋๋ค.
์ฝ 1000๊ฐ์ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ ๊ฒฝ์ฐ ๋ ๋๋ง์ ๋งค์ฐ ๋ฒ๋ฒ
์ด๊ฒ ๋๋ฉฐ ์ ์ ์๊ฒ๋ ์์ข์ ๊ฒฝํ์ผ๋ก ์๋ฟ์ต๋๋ค.
ํ์ฌ ์๊ฐ์ค์ธ ํด๊ฒฐ๋ฐฉ์์ ์๋์ ๊ฐ์ต๋๋ค.
- ํด๋ผ์ด์ธํธ : ๋ฌดํ์คํฌ๋กค์ ๊ตฌํํ์ฌ ํน์ ์์ ์ ์๋ฒ์ ๋ฐ์ดํฐ ์์ฒญ
- ์๋ฒ : ์ฟผ๋ฆฌ์คํธ๋ง์ ํตํด ๋ฐํํ ๋ฐ์ดํฐ์ ๋ฒ์๋ฅผ ๋ฐ๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธก์์ ํํฐ๋ง์ ํตํด ๋ฐ์ดํฐ ๋ฐํ
ํ์ฌ๋ Controller์ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฌธ์์ ํ์ํ ๋ด์ฉ์ ์ ์ํด๋ ์ํ์ ๋๋ค
ํ์ง๋ง ๋ฐ์ฝ๋ ์ดํฐ ๋ด๋ถ์ ๋ชจ๋ ๋ด์ฉ์ ์ ์ํ ๊ฒฝ์ฐ ์๋์ ๊ฐ์ด ์ฝ๋๊ฐ ๋งค์ฐ ๊ธธ์ด์ง๊ฒ ๋ฉ๋๋ค.
@ApiOperation({ summary: 'ํ์๊ฐ์
API' })
@ApiCreatedResponse({
description: `ํ์๊ฐ์
์ฑ๊ณต์ 201 ์ฝ๋์ ์์ฑ๋ ์ฌ์ฉ์์ ์์ด๋ ๋ฐํ`,
schema: {
type: 'object',
properties: { userId: { type: 'string' } },
},
})
@ApiBadRequestResponse({
description: `
์ด๋ฉ์ผ ํ์์ด ์ฌ๋ฐ๋ฅด์ง ์์๊ฒฝ์ฐ - invalid_email
๋น๋ฐ๋ฒํธ ํ์์ด ์ฌ๋ฐ๋ฅด์ง ์์๊ฒฝ์ฐ - invalid_password
๋๋ค์ ํ์์ด ์ฌ๋ฐ๋ฅด์ง ์์๊ฒฝ์ฐ - invalid_nickname
์ค๋ณต๋ ์ด๋ฉ์ผ์ธ ๊ฒฝ์ฐ - exist_email
์ค๋ณต๋ ๋๋ค์์ธ ๊ฒฝ์ฐ - exist_nickname`,
schema: {
type: 'object',
properties: {
statusCode: { type: 'number' },
message: {
example: 'invalid_email, invalid_password, invalid_nickname, exist_email, exist_nickname',
},
},
},
})
@UsePipes(ValidationPipe)
@Post('register')
async register(@Body() registerDto: RegisterDto) {
const userId = await this.authService.register(registerDto);
return { userId };
}Swagger ๋ฌธ์์ ํ์ํ ๋ด์ฉ์ ์ธ๋ถํ์ผ๋ก ๋ถ๋ฆฌํ์ฌ ์ฝ๋๋ฅผ ๊ฐ์ ํ ์์ ์ ๋๋ค.
@ApiRegister('ํ์๊ฐ์
API')
@UsePipes(ValidationPipe)
@Post('register')
async register(@Body() registerDto: RegisterDto) {
const userId = await this.authService.register(registerDto);
return { userId };
}์ฒ์ ๋์
ํ๊ณ ๊ฒฝํํ ๊ธฐ์ ๊ณผ ๊ฐ๋
๋ค์ด ๋ค์ด ๋งค์ฐ ๋ง์์ต๋๋ค.
TDD ๋ถํฐ ์์ํด์ CI/CD, AWS, ์๋ก์ด ํ๋ ์์ํฌ/๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฑ..
์๋ก์ด๊ฑธ ๋ฐฐ์ธ๋๋ง๋ค ๊ณ์ํด์ ๊ธฐ๋กํ๊ณ ๊พธ์คํ๊ฒ ๋
ธ๋ ฅํ์ต๋๋ค.
์ฒ์ ๋ง์ฃผํ๋ ๋ฌธ์ ๋๋ผ๋ ๊พธ์คํ ๋
ธ๋ ฅํ๋ฉด ํด๊ฒฐํ ์ ์๋ค๋ ์์ ๊ฐ์ ์ป์ ์ ์์์ต๋๋ค.