패키지 설치

npm install --save mysql2
npm install --save-dev @types/node

 

모듈화

import mysql, {ConnectionOptions, ResultSetHeader, RowDataPacket} from "mysql2/promise";
import "../config/env";
import {DatabaseError} from "../middleware/errors/DatabaseError";

const access: ConnectionOptions = {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    port: Number(process.env.DB_PORT || "3306"),
    waitForConnections: true,
    connectionLimit: 10,
    queueLimit: 0,
    connectTimeout: 10000, // MySQL 서버에 연결할 때의 타임아웃(ms 단위)
    enableKeepAlive: true // 연결이 일정 시간 동안 비활성 상태일 때에도 끊어지지 않도록
};

export const database = mysql.createPool(access);

export const selectList = async <T extends RowDataPacket>(statement: string): Promise<T[]> => {
    let connection;

    try {
        connection = await database.pool.promise().getConnection();
        const result = await connection.query<T[]>(statement);
        return result[0];
    } catch (e: unknown) {
        if (e instanceof Error) {
            throw new DatabaseError(e.message);
        }
        throw new Error("DB Unexpected Error");
    } finally {
        if (connection) {
            connection.release();
        }
    }
};

export const selectOne = async <T extends RowDataPacket>(statement: string): Promise<T> => {
    let connection;

    try {
        connection = await database.pool.promise().getConnection();
        const result = await connection.query<T[]>(statement)

        return result[0][0];
    } catch (e: unknown) {
        if (e instanceof Error) {
            throw new DatabaseError(e.message);
        }
        throw new Error("DB Unexpected Error");
    } finally {
        if (connection) {
            connection.release();
        }
    }
};

export const mutation = async (statement: string): Promise<ResultSetHeader> => {
    let connection;

    try {
        connection = await database.pool.promise().getConnection();
        const result = await connection.query<ResultSetHeader>(statement);
        return result[0];
    } catch (e: unknown) {
        if (e instanceof Error) {
            throw new DatabaseError(e.message);
        }
        throw new Error("DB Unexpected Error");
    } finally {
        if (connection) {
            connection.release();
        }
    }
}

 

사용 방법

import {mutation, selectList, selectOne} from "../../config/db";
import {ResultSetHeader} from "mysql2/promise";
import UserType from "../../types/UserType";

class UserModel {
	static async getAllUsers(): Promise<UserType[]> {
        const statement = `
          SELECT USER_ID
          FROM USER_TABLE
        `;

        return await selectList<UserType>(statement);
      }

      static async getUserById(USER_ID: string): Promise<UserType> {
        const statement = `
          SELECT USER_ID
          FROM USER_TABLE
          WHERE USER_ID = '${USER_ID}'
        `;

        return await selectOne<UserType>(statement);
      }

      static async createUser(user: UserType): Promise<ResultSetHeader> {
        const statement = `INSERT INTO ...`
        return await mutation(statement);
      }
  
      tatic async updateUser(user: UserType): Promise<ResultSetHeader> {
        const statement = `UPDATE ...`
        return await mutation(statement);
      }
      
      static async deleteUser(user: UserType): Promise<ResultSetHeader> {
        const statement = `DELETE ...`
        return await mutation(statement);
      }
  }

export default UserModel;

 

 


 

 

Using MySQL2 with TypeScript | Quickstart

Installation

sidorares.github.io

 

'JavaScript > TypeScript' 카테고리의 다른 글

[TS] Express 속성 타입 추가하기  (0) 2024.06.17

 

모듈 설치

npm i express
npm i @types/express

 

# src/middleware/guard.ts
import {NextFunction, Request, Response} from "express";

export default (req: Request, res: Response, next: NextFunction) => {
    try {
        ...
        
        req.session = {
            LOGIN_ID: decodeTokenJson.LOGIN_ID
        }
        next();
    } catch (e: unknown) {
        next(e);
    }
}

JWT 검증에 성공한다면,  Request 정보에 LOGIN_IN 속성을 추가하여 뒷 단에서 decode한 값들은 사용하려 한다

 

# src/types/express/index.d.ts
import * as express from "express"

declare global {
  namespace Express {
    interface Request {
      session: {
        LOGIN_ID: string;
      }
    }
  }
}

types/express/index.d.ts 파일을 생성하여, 원하는 속성 타입을 추가한다

 

# tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",          /* JavaScript 버전 */
    "module": "commonjs",        /* 모듈 시스템 */
    "outDir": "./dist",          /* 컴파일된 파일 출력 디렉토리 */
    "rootDir": "./src",          /* 소스 파일 디렉토리 */
    "strict": true,              /* 엄격한 타입 검사 */
    "esModuleInterop": true,     /* ES 모듈과의 상호 운용성 */
    "skipLibCheck": true,         /* 라이브러리 체크 건너뛰기 */
    "typeRoots": ["./src/types", "./node_modules/@types"]
  }
}

typeRoots 속성을 추가한다. 중요한 것은  node_modules/@types 의 값이 나중에 선언되어야 적용된다

'JavaScript > TypeScript' 카테고리의 다른 글

[TS] mysql2 패키지 모듈화  (0) 2024.06.17

 

Redux 상태 관리 라이브러리

 

저장된 데이터를 전역에서 관리

 


NPM 설치

npm i redux
npm i react-redux
npm i @reduxjs/toolkit

 

폴더 구조 (expo)

 

App.tsx

import { Provider } from "react-redux";
import Main from "./src/screens/Main";
import store from "./src/store";

export default function App() {
  return (
    <Provider store={store}>
      <Main />
    </Provider>
  );
}

 

src/slices/counter.ts

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  count: 0,
};

const countSlice = createSlice({
  name: "count",
  initialState,
  reducers: {
    plusCount(state) {
      state.count += 1
    },
    minusCount(state) {
      state.count = state.count - 1;
    },
    setCount(state, action) {
      state.count = action.payload.count;
    },
  },
});

export default countSlice;

 

src/store/reducer.ts

import { combineReducers } from "redux";
import countSlice from "../slices/counter";

const rootReducer = combineReducers({
    countReducer: countSlice.reducer
});

export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;

 

src/store/index.ts

import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducer";
import { useDispatch } from "react-redux";

const store = configureStore({
    reducer: rootReducer,
    middleware: getDefaultMiddleware => {
        return getDefaultMiddleware();
    }
});

export default store;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();

 

src/screens/Main.tsx

import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { useSelector } from "react-redux";
import countSlice from "../slices/counter";
import { useAppDispatch } from "../store";
import { RootState } from "../store/reducer";

const Main = () => {
  const dispatch = useAppDispatch();
  const count = useSelector((state: RootState) => state.countReducer.count);

  const setCounter = (value: number) => {
    dispatch(
      countSlice.actions.setCount({
        count: value,
      })
    );
  };

  return (
    <View style={styles.container}>
      <View style={styles.view_container}>
        <Text>{count}</Text>
      </View>
      <View style={styles.button_container}>
        <TouchableOpacity
          style={StyleSheet.compose(styles.button, { backgroundColor: "blue" })}
          onPress={() => dispatch(countSlice.actions.plusCount())}
        >
          <Text style={styles.button_text}>+</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={StyleSheet.compose(styles.button, { backgroundColor: "red" })}
          onPress={() => {
            if (count > 0) {
              dispatch(countSlice.actions.minusCount());
            }
          }}
        >
          <Text style={styles.button_text}>-</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  view_container: {
    flex: 1,
    justifyContent: "center",
  },
  button_container: {
    flex: 1,
    flexDirection: "row",
  },
  button: {
    width: 100,
    height: 100,
    alignItems: "center",
    justifyContent: "center",
    borderRadius: 20,
    margin: 20,
  },
  button_text: {
    fontWeight: "bold",
    color: "#ffffff",
    fontSize: 30,
  },
});

export default Main;

 

 


 

 

Installation | Redux

Introduction > Installation: Installation instructions for Redux and related packages

redux.js.org

 

'JavaScript > React Native' 카테고리의 다른 글

[React Native] 프로젝트 생성  (0) 2024.01.14

리액트 네이티브 (React Native)는 페이스북에서 개발한 오픈 소스 프레임워크

JavaScript (+ TypeScript), React를 사용하여 안드로이드, iOS 앱을 개발할 수 있다

 

일반적으로 하나의 코드 작성을 통해 안드로이드, iOS 앱을 개발할 수 있지만, 각 플랫폼마다 지원하는 함수가 다른 경우가 있다

 

프로젝트 방식에는 React Native CLIExpo 방식이 있다

 

Expo 방식은  안드로이드, IOS 에서 Expo Go 어플을 통해서 같이 네트워크 안에 있다면 QR 코드로 쉽게 스마트폰에서 실시간으로 확인하면서 쉽게 개발할 수 있고, Expo에서 제공하는 모듈들이 많기 때문에 편했다

그리고 확실히 개발 속도가 빠르다는 것을 느꼈다

 

React Native CLI 방식을 이용하면 안드로이드 스튜디오에서 제공하는 에뮬레이터와 XCode의 시뮬레이터를 통해 실시간으로 확인하면서 개발이 가능하고, 그렇지 않다면 스마트폰에 USB를 연결해서 확인도 가능하다. (Expo도 에뮬레이터, 시뮬레이터로 확인 가능)

처음 시작했을때 환경 설정하는 것도 어렵고, android, ios 폴더를 별도로 있어 파일의 양이 많다. 환경 구축이 되어 있지 않다면 초기에 어려운 과정으로 인해 개발 속도가 매우 느릴 수 있음

하지만 안드로이드, IOS의 네이티브 모듈을 건들기 위해서는 CLI 방식을 택해야 한다

 

외주 프로젝트를 시작할 때,  개발 속도가 빠르고 편한 Expo 방식을 사용했다

문제는 Expo에서 제공하는 무료 배포 방식을 사용했었는데, 저녁 시간 대 사용하면 1시간 넘게 기다리기도 했다

마냥 대기 시간을 기다려야 했기 때문에 빠르게 포기하고,  React Native CLI로 변경해서 개발했다

 

Expo에서 유료 플랜을 사용하면, 배포 속도가 빠르다고 하는데 가격이 비싼 것 같다

실제 서비스라면 React Native CLI로 개발하는 것이 좋을 것 같다

 

프로젝트 생성

# React Native CLI  *
npx react-native init [project name]
npx react-native init specificVerProject --version=0.69 # 특정 버전 설치

# expo 설치
npm install --global expo-cli

# expo project 생성
npx create-expo-app # javascript
npx create-expo-app --template # typescript

# bun
bunx create-expo-app # javascript

 

 

프로젝트 실행

npm run start
npm run android
npm run ios

 

  • 안드로이드인 경우, 안드로이드 스튜디오와 에뮬레이터 (Emulator)를 설치
  • iOS인 경우, Xcode와 시뮬레이터 (Simulator)를 설치

 

'JavaScript > React Native' 카테고리의 다른 글

[React Native] Redux 상태 관리  (0) 2024.01.21
// package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "NODE_ENV=DEV node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
// index.js
console.log(process.env.NODE_ENV);
npm run start

 

MacOS 환경에서 start 스크립트를 실행하면 파일에서 선언한 NODE_ENV 값(DEV)을 사용할 수 있다

하지만 윈도우OS 환경에서 start 스크립트를 실행하면 값이 제대로 들고 오지 못할 때가 있다

 

npm run start
// package.json
{
  "scripts": {
    "start": "cross-env NODE_ENV=DEV node index.js"
}

 

cross-env 패키지 설치 후, 실행하면 윈도우 환경에서도 환경 변수 값을 사용할 수 있다

// package.json
{
  "scripts": {
    "start:windows": "cross-env NODE_ENV=DEV node index.js",
    "start:mac": "NODE_ENV=DEV node index.js"
}

'JavaScript > Node' 카테고리의 다른 글

PM2를 활용한 앱 관리  (0) 2023.08.05
[Node Express] PayloadTooLargeError  (0) 2023.05.11
카페24 노드 서버 구축하기  (2) 2020.12.01

Guard

Guard Process

 

Guard는 '경비' 라는 뜻대로, Application을 보호하고, 유효한 사용자(authentication, 인증: 애플리케이션의 유효한 사용자인지 확인), 권한(authorization, 인가: 사용자에게 허가된 작업인지 확인)을 검증하는 역할을 한다. 검증된 사용자가 아니면 요청 자체를 막아버리는 것이다. 예를 들면, 사용자에 대한 검증이 없다면 모든 요청에 대해 응답을 할 것이고, 서버 자원에 낭비가 지속되면 서버 부하 / 다운의 결과를 초래할 가능성이 높다.

 

일반적으로 기존 Express.js 서버에서는 미들웨어(middleware)에 의해 처리되었다. 미들웨어는 인증을 위한 휼륭한 선택이지만, 토큰 검증이나 속성 연결과 같은 작업을 관리하기 힘들다.

→ 가드는 모든 미들웨어 이후에 실행되지만 인터셉터나 파이트 이전에 실행된다

 

JWT(Json Web Token)을 활용하여, 사용자 인증과 인가를 구현하는 방법을 작성하였다.

 

  1. 사용자 로그인 성공 시, AccessToken과 RefreshToken을 발급
  2. 로그인 이후 사용자는 Request Header에 AccessToken을 포함하여 요청
  3. 사용자는 AccessToken이 만료되면, 발급 받았던 RefreshToken을 통해 새로운 AccessToken을 발급

추가적으로 사용자 인증 전에는 AccessToken을 가지고 있지 않기 때문에, 로그인 로직에서는 Guard 검증을 거치지 않도록 설정

 


NPM 패키지 설치

npm i --save @nestjs/jwt

📔 src/auth/guard/public.decorator.ts

import { SetMetadata } from "@nestjs/common";

export const Public = () => SetMetadata('isPublic', true)

사용자 로그인 이전에는 Jwt 토큰이 없기 때문에, 함수 데코레이터 선언을 통해 로그인 API에서는 인증 가드 검증을 하지 않도록 할 예정

 

Auth Module, Service, Controller 작성

 

📔 src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { UserModule } from '@src/user/user.module';

@Module({
  imports: [JwtModule.register({
    global: true
  }), UserModule],
  providers: [AuthService],
  controllers: [AuthController]
})
export class AuthModule { }

 

📔 src/auth/user.module.ts

더보기
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule { }

UserService를 exports 설정

 

 

📔 src/auth/auth.service.ts

더보기
import { Injectable, InternalServerErrorException, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '@src/user/user.service';

@Injectable()
export class AuthService {

    private readonly ACCESS_TOKEN_SECRET_KEY: string;
    private readonly REFRESH_TOKEN_SECRET_KEY: string;

    constructor(private readonly jwtService: JwtService
        , private readonly userService: UserService
        , private readonly configService: ConfigService
    ) {
        this.ACCESS_TOKEN_SECRET_KEY = configService.get<string>('SERVER.ACCESS_TOKEN_SECRET_KEY')
        this.REFRESH_TOKEN_SECRET_KEY = configService.get<string>('SERVER.REFRESH_TOKEN_SECRET_KEY')
    }

    async signIn(id: string, password: string) {
        // ID를 가진 사용자가 있는지 데이터베이스 조회
        const user = await this.userService.signIn(id);

        if (!user) {
            throw new UnauthorizedException('유저 정보 없음');
        } else if (user?.password !== password) {
            throw new UnauthorizedException('비밀번호 일치하지 않음');
        }

        const payload = { id: user.id, name: user.name }

        return {
            accessToken: await this.jwtService.signAsync(payload, {
                secret: this.ACCESS_TOKEN_SECRET_KEY
                , expiresIn: '10m'
            }),
            refreshToken: await this.jwtService.signAsync(payload, {
                secret: this.REFRESH_TOKEN_SECRET_KEY
                , expiresIn: '2w'
            })
        }
    }

    async getNewAccessToken(refreshToken: string) {
        try {
            // RefreshToken 검증
            const payload = await this.jwtService.verifyAsync(
                refreshToken,
                {
                    secret: this.REFRESH_TOKEN_SECRET_KEY
                }
            )

            // 새로운 AccessToken 발급
            return {
                accessToken: await this.jwtService.signAsync({
                    id: payload.id,
                    name: payload.name
                }, {
                    secret: this.ACCESS_TOKEN_SECRET_KEY
                    , expiresIn: '10m'
                })
            }
        } catch (e) {
            if (e.name === 'TokenExpiredError') {
                throw new UnauthorizedException('refreshToken 만료');
            } else if (e.name === 'JsonWebTokenError') {
                throw new UnauthorizedException('refreshToken 정보 없음');
            }

            throw new InternalServerErrorException(e);
        }
    }

}
  • ConfigService를 통해 환경 변수 관리 (ACCESS_TOKEN_SECRET_KEY,  REFRESH_TOKEN_SECRET_KEY)
  • 토큰 만료기간은 AccessToken은 짧게, RefreshToken은 길게 설정
  • JWT 토큰의 Payload(내용)는 사용자의 ID, 이름으로 구분

 

 

 

[NestJS] 환경 변수 (config) 사용하기

환경 변수, 파일을 사용하는 이유 - 프로젝트에서 공통적으로 사용하는 변수들을 파일을 통해 쉽게 관리할 수 있다 - 코드에 secret key 값을 노출시키지 않기 때문에 보안 상 이점이 있다 - 운영(pro

clichy12.tistory.com

 

 

📔 src/auth/auth.controller.ts

더보기
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Public } from './guard/public.decorator';

@Controller('auth')
export class AuthController {

    constructor(
        private readonly authService: AuthService
    ) { }

    @HttpCode(HttpStatus.OK)
    @Post('sign-in')
    @Public()
    signIn(@Body() user: Record<string, any>) {
        return this.authService.signIn(user.id, user.password);
    }

    @HttpCode(HttpStatus.OK)
    @Post('refresh')
    @Public()
    getNewAccressToken(@Body() body: Record<string, any>) {
        return this.authService.getNewAccessToken(body.refreshToken);
    }

}

Public 데코레이터를 선언하여,  로그인이나 새로운 AccessToken을 발급 받는 API 요청 시에는 인증 가드 검증을 하지 않도록 설정

 


Auth Guard 작성

 

📔 src/auth/guard/Auth.guard.ts

import { CanActivate, ExecutionContext, Injectable, InternalServerErrorException, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { Request } from "express";


@Injectable()
export class AuthGuard implements CanActivate {

    private readonly ACCESS_TOKEN_SECRET_KEY: string;

    constructor(private jwtService: JwtService
         , private configService: ConfigService
         , private reflector: Reflector
        ) {
            this.ACCESS_TOKEN_SECRET_KEY = configService.get<string>('SERVER.ACCESS_TOKEN_SECRET_KEY')
        }

    async canActivate(context: ExecutionContext): Promise<boolean>{
        // Public 데코레이터를 선언한 라우터 경로는 JWT 토큰 검증을 하지 않음 (ex. 로그인)        
        const isPublic = this.reflector.get('isPublic', context.getHandler());
        if(isPublic){
            return true;
        }

        const request = context.switchToHttp().getRequest();

        // Request Header Authorization. JWT AccessToken 추출
        const token = this.extractTokenFromHeader(request);
        
        if(!token) {
            throw new UnauthorizedException('accessToken 필요');
        }

        try {
            // AccessToken 검증
            const payload = await this.jwtService.verifyAsync(
                token,
                {
                    secret: this.ACCESS_TOKEN_SECRET_KEY
                }
            )

            // 라우트 핸들러에서 접근할 수 있도록 Request에 payload를 할당
            request['user'] = payload;
        } catch (e){            
            if (e.name === 'TokenExpiredError') {
                throw new UnauthorizedException('accessToken 만료');
            } else if (e.name === 'JsonWebTokenError') {
                throw new UnauthorizedException('accessToken 정보 없음');
            }
            
            throw new InternalServerErrorException(e);
        }
        
        return true;
    }

    private extractTokenFromHeader(request: Request) : string | undefined {
        const [type, token] = request.headers.authorization?.split(' ') ?? [];
        return type === 'Bearer' ? token : undefined;
    }
}
  • Public 데코레이터를 선언한 라우터 경로는 검증 하지 않음
  • Header Authorization에 있는 JWT AccessToken 검증
  • 비즈니스 로직에서 JWT 정보를 사용할 수 있도록, Request에 Payload를 할당
  • canActivate 함수에서 false를 반환하면  403 Forbidden Error를 반환
  • 토큰 만료 시, 401 Unauthorized Error를 반환하도록 설정
  @Get()
  findAll(@Req() req: Request) {
    const user = req['user'];
    console.log(user);
    
    return this.productService.findAll();
  }

 

 

📔 src/app.module.ts

	...
import { APP_GUARD } from '@nestjs/core';
import { AuthModule } from './auth/auth.module';
import { AuthGuard } from './auth/guard/Auth.guard';

@Module({
  imports: [
                ...
                AuthModule
        	],
  controllers: [
  				...
  ],
  providers: [
  				...
  			{
            provide: APP_GUARD,
            useClass: AuthGuard
		 }],
})
export class AppModule implements NestModule {
	...
}

accessToken 없이 요청 시, 401 Error 발생
로그인 성공 시, 클라이언트에게 accessToken, refreshToken을 반환
발급 받은 AccessToken 값을 요청 헤더에 포함하여 요청 &rarr; 인증 가드 검증 성공
accessToken 만료 시, 클라이언트는 발급 받은 refreshToken을 통해 새로운 accessToken을 발급

 


+ Redis (NoSQL), RDBMS 를 통해  accessToken, refreshToken 관리

+ 사용자 로그아웃 시, JWT Token 폐기 로직


참고 링크

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

 

🌐 JWT 토큰 인증 이란? (쿠키 vs 세션 vs 토큰)

Cookie / Session / Token 인증 방식 종류 보통 서버가 클라이언트 인증을 확인하는 방식은 대표적으로 쿠키, 세션, 토큰 3가지 방식이 있다. JWT를 배우기 앞서 우선 쿠키와 세션의 통신 방식을 복습해

inpa.tistory.com

 

 

[NestJS] Guard 사용하기

웹서버에서 중요한 것 중의 하나는 보안 관련한 로직일 것이다. NestJS에서는 허용된 User가 아니면 query나 Mutation, Subscription을 요청하지 못하도록 하는 Middleware를 제공한다. 이것을 UseGuard라는 decora

tre2man.tistory.com

 

'JavaScript > NestJS' 카테고리의 다른 글

[NestJS] Exception filters  (0) 2023.08.21
[NestJS] Swagger 적용하기  (0) 2023.08.21
[NestJS] TypeORM CRUD 생성하기 (User)  (0) 2023.08.21
[NestJS] 환경 변수 (config) 사용하기  (0) 2023.08.20
[NestJS] Logging 하기  (0) 2023.08.19

 

Client Side &rarr; Pipe &rarr; Route Handler &rarr; Filter &rarr; Client Side 과정을 거친다

 

NestJS는 예외 레이어(exceptions layer)가 내장되어 있어서, 애플리케이션 전체에서 처리되지 않은 예외를 처리할 수 있다

 

애플리케이션 (Application Code) 예외 처리

  • O : Exception Filter 에서 처리하지 않는다
  • X : Filter 에서 예외를 포착하여 적절한 사용자 친화적인 응답을 자동으로 보낸다

 


Global Exceptions Filter 생성

 

📔 src/middleware/exception/HttpExceptionsFilter.ts

@Catch() 데코레이터의 매개변수 목록을 비워, 처리되지 않은 모든 예외를 포착할 수 있다

import { ArgumentsHost
    , Catch
    , ExceptionFilter
    , HttpException
    , HttpStatus } from "@nestjs/common";
import { HttpAdapterHost } from "@nestjs/core";

@Catch()
export class HttpExceptionsFilter implements ExceptionFilter {
    constructor(private readonly httpAdapterHost: HttpAdapterHost) { }

    catch(exception: unknown, host: ArgumentsHost): void {
        const { httpAdapter } = this.httpAdapterHost;

        const ctx = host.switchToHttp();
        
        const httpStatus =
            exception instanceof HttpException
                ? exception.getStatus()
                : HttpStatus.INTERNAL_SERVER_ERROR; 
        
        const responseBody = {
            statusCode: httpStatus,
            timestamp: new Date().toLocaleString(),
            path: httpAdapter.getRequestUrl(ctx.getRequest()),
            message: "Http Exceptions Filtering",
            message1: "message1"
        }

        httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
    }
}

 

📔 src/main.ts

	...
    
import { HttpExceptionsFilter } from '@src/middleware/exception/HttpExceptionsFilter'


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

  const httpAdapter = app.get(HttpAdapterHost);
  app.useGlobalFilters(new HttpExceptionsFilter(httpAdapter));

	...

  await app.listen(port);
}
bootstrap();

 

Exception Layer Filtering

 


참고 링크

https://docs.nestjs.com/exception-filters

https://tristy.tistory.com/52

'JavaScript > NestJS' 카테고리의 다른 글

[NestJS] Guard + JWT 구현하기  (0) 2023.08.27
[NestJS] Swagger 적용하기  (0) 2023.08.21
[NestJS] TypeORM CRUD 생성하기 (User)  (0) 2023.08.21
[NestJS] 환경 변수 (config) 사용하기  (0) 2023.08.20
[NestJS] Logging 하기  (0) 2023.08.19

 

Swagger(스웨거)는 개발자가 REST 웹 서비스를 설계, 빌드, 문서화, 소비하는 일을 도와주는 대형 도구 생태계의 지원을 받는 오픈 소스 소프트웨어 프레임워크이다. 대부분의 사용자들은 Swagger UI 도구를 통해 스웨거를 식별하며 툴셋에는 자동화된 문서화, 코드 생성, 테스트 케이스 생성 지원이 포함된다. - wikipedia

 

 

NPM 패키지 설치

$ npm install --save @nestjs/swagger

 

 

src/config/swagger.config.ts

import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"

export default app => {
    const config = new DocumentBuilder()
        .setTitle('nest project')
        .setDescription('description')
        .setVersion('1.0.0')
        .build();

    const document = SwaggerModule.createDocument(app, config);
    SwaggerModule.setup('swagger', app, document);
}
  • http://localhost:3000/swaggerSwagger UI 접속

 

 

DocumentBuilder Options

더보기
import { OpenAPIObject } from './interfaces';
import { ExternalDocumentationObject, ParameterObject, SecurityRequirementObject, SecuritySchemeObject, ServerVariableObject } from './interfaces/open-api-spec.interface';
export declare class DocumentBuilder {
    private readonly logger;
    private readonly document;
    setTitle(title: string): this;
    setDescription(description: string): this;
    setVersion(version: string): this;
    setTermsOfService(termsOfService: string): this;
    setContact(name: string, url: string, email: string): this;
    setLicense(name: string, url: string): this;
    addServer(url: string, description?: string, variables?: Record<string, ServerVariableObject>): this;
    setExternalDoc(description: string, url: string): this;
    setBasePath(path: string): this;
    addTag(name: string, description?: string, externalDocs?: ExternalDocumentationObject): this;
    addSecurity(name: string, options: SecuritySchemeObject): this;
    addGlobalParameters(...parameters: ParameterObject[]): this;
    addSecurityRequirements(name: string | SecurityRequirementObject, requirements?: string[]): this;
    addBearerAuth(options?: SecuritySchemeObject, name?: string): this;
    addOAuth2(options?: SecuritySchemeObject, name?: string): this;
    addApiKey(options?: SecuritySchemeObject, name?: string): this;
    addBasicAuth(options?: SecuritySchemeObject, name?: string): this;
    addCookieAuth(cookieName?: string, options?: SecuritySchemeObject, securityName?: string): this;
    build(): Omit<OpenAPIObject, 'paths'>;
}

 

 

SwaggerModule.setup의 첫번째 매개변수는 swagger path를 의미

 

 

src/main.ts

	...
import swagger from './config/swagger.config';

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

	...

  swagger(app);

  await app.listen(port);
}
bootstrap();

 

 

src/app/app.controller.ts  →  @ApiTags. @ApiOperation. @ApiOkResponse. @ApiBadRequestResponse

	...

@ApiTags('app tag')
@Controller()
export class AppController {

  constructor(private readonly appService: AppService) { }

  @ApiOperation({
    summary: '서버 상태 검사',
    description: '정상적으로 서버가 실행되고 있는지 확인'
  })
  @ApiOkResponse({
    description: 'OK'
  })
  @ApiBadRequestResponse({
    description: 'Bad Request'
  })
  @Head()
  getStatus() {
    this.appService.getStatus();
  }
  
  	...

 

 

swagger ui 확인

 

 

 


참고 링크

https://docs.nestjs.com/openapi/introduction

https://any-ting.tistory.com/122

https://www.npmjs.com/package/@nestjs/swagger

'JavaScript > NestJS' 카테고리의 다른 글

[NestJS] Guard + JWT 구현하기  (0) 2023.08.27
[NestJS] Exception filters  (0) 2023.08.21
[NestJS] TypeORM CRUD 생성하기 (User)  (0) 2023.08.21
[NestJS] 환경 변수 (config) 사용하기  (0) 2023.08.20
[NestJS] Logging 하기  (0) 2023.08.19

TypeORM 패키지 설치

$ npm install --save @nestjs/typeorm typeorm mysql2

 

MySQL 설치

 

[MacOS] MySQL 설치

brew 설치 $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" Homebrew Site MySQL 설치, 설정 # Installation $ brew install mysql # version $ mysql -V # server start $ mysql.server start # server stop $ my

clichy12.tistory.com

 

User 테이블 생성

CREATE TABLE `User` (
  `idx` bigint unsigned NOT NULL AUTO_INCREMENT,
  `id` varchar(100) NOT NULL,
  `password` varchar(100) NOT NULL,
  `name` varchar(100) NOT NULL,
  `createDt` timestamp NULL DEFAULT NULL,
  `updateDt` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`idx`)
)

 


 

# res: resource
$ nest g res user

spec.ts 파일은 삭제


src/config/typeorm.config.service.ts

더보기
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from "@nestjs/typeorm";
import { ConfigService } from "@nestjs/config";
import { Injectable } from "@nestjs/common";

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {

    constructor(private readonly configService: ConfigService) { }

    createTypeOrmOptions(connectionName?: string): TypeOrmModuleOptions | Promise<TypeOrmModuleOptions> {

        return {
            type: 'mysql',
            host: this.configService.get('DB.MYSQL.HOST'),
            port: this.configService.get('DB.MYSQL.PORT'),
            username: this.configService.get('DB.MYSQL.USER'),
            password: String(this.configService.get('DB.MYSQL.PASSWORD')),
            database: this.configService.get('DB.MYSQL.DATABASE'),
            entities: [],
            synchronize: false,
            autoLoadEntities: true
        }
    }

}

 

entities : TypeORM에 Entity를 명시적으로 알린다, autoLoadEntities: true 를 사용하기 때문에 빈 배열을 넣었다

synchronize : 운영 환경에서 true 값을 사용하게 되면, 데이터가 손상될 수 있다

autoLoadEntities : true 일 경우, Entity가 자동으로 로드된다. forFeature() 선언을 통해 자동으로 추가될 예정

  • user.module.ts에서 TypeOrmModule.forFeature를 통해 선언 할 예정

src/app.module.ts

더보기
...
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmConfigService } from './config/typeorm.config.service';

@Module({
  imports: [
  		...
  	, TypeOrmModule.forRootAsync({
      useClass: TypeOrmConfigService
    })],
	...
})
export class AppModule implements NestModule {
	...
}

src/user/entities/user.entity.ts

더보기
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    idx: number;

    @Column()
    id: string;

    @Column()
    password: string;

    @Column()
    name: string;

    @Column()
    createDt: Date;

    @Column()
    updateDt: Date;
}

 

데이터베이스 User 테이블과 동일한 속성으로 선언


src/user/user.module.ts

더보기
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

$ npm i class-validator

 

데코레이터(@) 선언만으로  API 요청 시, 데이터 검증을 자동으로 해준다

@IsOptional()을 선언하지 않으면 해당 속성은 필수 속성이 된다

 


src/user/dto/create-user.dto.ts

더보기
import { IsDate, IsOptional, IsString, Length } from 'class-validator';

export class CreateUserDto {

    @Length(5, 50)
    @IsString()
    readonly id: string;

    @IsString()
    readonly password: string;

    @IsString()
    readonly name: string;

    @IsOptional()
    @IsDate()
    readonly createDt: Date = new Date();

    @IsOptional()
    @IsDate()
    readonly updateDt: Date = new Date();

}

src/user/dto/update-user.dto.ts

더보기
import { IsDate, IsOptional, IsString } from 'class-validator';

export class UpdateUserDto {

    @IsOptional()
    @IsString()
    readonly password: string;

    @IsString()
    readonly name: string;

    @IsOptional()
    @IsDate()
    readonly updateDt: Date = new Date();

}

src/user/user.service.ts

더보기
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Repository } from 'typeorm';

@Injectable()
export class UserService {

  constructor(@InjectRepository(User) private userRepository: Repository<User>) { }

  async create(createUserDto: CreateUserDto) {
    const user = await this.findOne(createUserDto.id);

    if (user.length > 0) {
      return [];
    }

    return await this.userRepository.save(createUserDto);
  }

  async findAll() {
    return await this.userRepository.find();
  }

  async findOne(id: string) {
    return await this.userRepository.find({ select: ['idx', 'id', 'name', 'createDt', 'updateDt'], where: { id } })
  }

  update(id: string, updateUserDto: UpdateUserDto) {
    console.log(id, updateUserDto);

    return this.userRepository.update({ id }, updateUserDto);
  }

  remove(id: string) {
    return this.userRepository.delete({ id });
  }
}

src/user/user.controller.ts

더보기
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }

  @Get()
  findAll() {
    return this.userService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.userService.update(id, updateUserDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.userService.remove(id);
  }
}

$ npm i class-transformer

 

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

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

ValidationPipe 선언만으로 검증을 해줄 수 있다

 

 


참고 링크

https://docs.nestjs.com/techniques/validation

https://velog.io/@artlogy/08.-Validate

https://docs.nestjs.com/techniques/database

https://codegear.tistory.com/116

'JavaScript > NestJS' 카테고리의 다른 글

[NestJS] Exception filters  (0) 2023.08.21
[NestJS] Swagger 적용하기  (0) 2023.08.21
[NestJS] 환경 변수 (config) 사용하기  (0) 2023.08.20
[NestJS] Logging 하기  (0) 2023.08.19
[NestJS] Jest 테스트하기  (0) 2023.08.17

환경 변수, 파일을 사용하는 이유

- 프로젝트에서 공통적으로 사용하는 변수들을 파일을 통해 쉽게 관리할 수 있다

- 코드에 secret key 값을 노출시키지 않기 때문에 보안 상 이점이 있다

- 운영(prod), 개발(dev), 로컬(local) 환경에 맞는 다른 변수를 사용할 수 있다

 


패키지 설치

$ npm i @nestjs/config

 

src/app.module.ts

import { ConfigModule } from '@nestjs/config';
...

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      cache: true
    }),
    ....]
...

루트 모듈의 설정을 통해 환경 변수에 접근할 수 있도록 할 수 있다

isGlobal: true 속성을 통해 다른 모듈에서 사용할 수 있도록 설정하였고, cache: true 속성을 통해 한 번 읽은 환경 변수의 값을 캐싱하여 읽기 속도를 향상 시킬 수 있다

 

위 설정만으로 .env 파일에 선언된 속성을 바로 사용할 수 있다

 

프로젝트 루트 경로/.env

NODE_ENV=DEV
SERVER_PORT=3000

 

 

main.ts

	...

const SERVER_PORT = process.env.SERVER_PORT 

async function bootstrap() {
	...
  await app.listen(SERVER_PORT);
} 
bootstrap();

개발 환경에 따른 환경 변수 사용 방법

 

.env

NODE_ENV=DEV

 

src/config/dev.yaml 또는 prod.yaml

SERVER:
  PORT: 3000

DB:
  MYSQL:
    HOST: 127.0.0.1
    USER: root
    PASSWORD: root
    PORT: 3306

 

src/config/config.ts

import { readFileSync } from 'fs'
import * as yaml from 'js-yaml'
import { join } from 'path'

const YAML_CONFIG_PROD = 'prod.yaml'
const YAML_CONFIG_DEV = 'dev.yaml'

export default () => {
    return yaml.load(
        process.env.NODE_ENV === 'DEV' ?
            readFileSync(join(__dirname, YAML_CONFIG_DEV), 'utf8') :
            readFileSync(join(__dirname, YAML_CONFIG_PROD), 'utf8')
    ) as Record<string, any>
}

 

src/app.module.ts

...
import config from './config/config';

@Module({
  imports: [ConfigModule.forRoot({
      isGlobal: true,
      cache: true,
      load: [config]
    })],
  ...
})

 

src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';

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

  const configService = app.get(ConfigService);
  const SERVER_PORT = configService.get<string>('SERVER.PORT');
  
  await app.listen(SERVER_PORT);
} 
bootstrap();

 

 

$ npm run start:dev

서버 실행

 

yaml 파일은 compile 시에, dist 폴더로 복사되지 않기 때문에 파일을 찾지 못하는 문제가 발생

 

패키지 설치

$ npm i cpx

 

 

package.json

...
"scripts": {
    "copy-files": "cpx \"src/config/*.yaml\" dist/config/",
    ...
    "start": "npm run copy-files && nest start",
    "start:dev": "npm run copy-files && nest start --watch",
    "start:debug": "npm run copy-files && nest start --debug --watch",
    "start:prod": "npm run copy-files && node dist/main",
    ...
  },
  ...

컴파일 시에 dist/config 경로에 src/config/*.yaml 파일이 위치할 수 있도록 복사해준다

 

 

nest-cli.json  

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": false
  }
}

package.json - scripts 수정 후 실행해도 똑같은 오류가 발생한다. 파일이 복사되지만 바로 파일이 삭제되기 때문이다

deleteOutDir 속성 값을  true false 로 변경해 준다

 


참고 링크

https://codegear.tistory.com/82

https://www.daleseo.com/nestjs-configuration/

https://velog.io/@kimjiwonpg98/NEST-.env-%ED%8C%8C%EC%9D%BC-%EA%B4%80%EB%A6%AC%EB%B2%95-feat.-nestjsconfig

'JavaScript > NestJS' 카테고리의 다른 글

[NestJS] Swagger 적용하기  (0) 2023.08.21
[NestJS] TypeORM CRUD 생성하기 (User)  (0) 2023.08.21
[NestJS] Logging 하기  (0) 2023.08.19
[NestJS] Jest 테스트하기  (0) 2023.08.17
[NestJS] 경로 alias 설정하기  (0) 2023.08.17

+ Recent posts