[Unity] Coroutine
코루틴이란?코루틴(Coroutine)은 Unity에서 사용되는 특수한 함수로, 일반 함수와 달리 실행을 중단하고 나중에 다시 재개할 수 있다. 코루틴은 IEnumerator 타입을 반환하고 yield 키워드를 사용하여 실행을 중단하고 특정 조건이 충족되면 다시 실행을 재개며, 이를 통해 시간 기반 작업이나 비동기 작업을 쉽게 처리할 수 있다.  다양한 yield 유형 지정된 시간(초) 동안 대기yield return new WaitForSeconds(3f); // 3초 동안 대기 현재 프레임의 끝까지 대기, 다음 프레임에서 코루틴이 재개된다.yield return null; 모든 렌더링 작업이 끝날 때까지 대기한다.yield return new WaitForEndOfFrame(); 다음 물리 업데이트가 ..
2024.06.28
no image
Please add the "priority" property if this image is above the fold.
Image 태그에서 해당 경고 발생했다.LCP 에  대한 경고이다. Priority 속성priority 속성은 Image 컴포넌트를 고우선 순위 리소스로 취급하며, 해당 이미지가 lazy loading 되지 않고 즉시 로딩되게 한다. 이 속성은 주로 화면의 상단에 표시되는 이미지에 유용하다.  priority 를 true로 설정하면 경고 사라진다.  장점priority 속성의 장점LCP(Largest Contentful Paint) 향상: 이미지가 즉시 로드되므로 LCP 지표가 개선다. LCP는 Core Web Vitals의 중요한 요소이다.사용자 경험 개선: 중요한 이미지를 빠르게 로드함으로써 초기 페이지 로딩 시 사용자 경험이 향상됩니다.
2024.05.22
no image
[Prisma] prisma 소개
Prisma 란?Prisma는 자바스크립트와 타입스크립트에서 주목받고 있는 차세대 ORM 도구이다.Prisma의 공식 홈페이지에선 자신을 이렇게 소개한다.Next-generation Node.js and TypeScript ORM Prisma는 Node.js, TypeScript, Rust, Go 등 다양한 언어와 프레임워크에서 사용할 수 있는 ORM (Object-Relational Mapping) 도구이다. 이는 개발자가 SQL 쿼리를 직접 작성하지 않고도 데이터베이스를 쉽게 다룰 수 있도록 도우며, 데이터베이스 초보자에게 더 쉽게 접근할 수 있게 도움을 주며, 능숙한 개발자들에게는 생산성을 높여준다.   Prisma의 작동Prisma ORM를 사용하는 모든 프로젝트는 Prisma Schema 파일로..
2024.04.28
no image
[NestJS] 구글 로그인 (2)/JWT 발급
JWT 발급 일반적으로는 이전 포스트까지 완료됐다면 DB에 해당 유저가 있는지 확인 후 없으면 DB에 저장, 있으면 JWT를 발급해서 로그인을 시켜야 한다. DB에 관련된 부분은 최대한 건너뛰고 JWT 관련된 것들만 작성하겠다. userService 작성 JWT 발급 전 회원가입이 되어 있는지 확인하기 위해서 DB를 확인해야 한다. 이때 userService에 로직을 작성한다. // user.service.ts import { Injectable } from "@nestjs/common"; export interface GoogleUser { id: string; provider: string; name: string; email: string; photo: string; } @Injectable() e..
2024.03.03
no image
[NestJS] JWT 모듈
JWT jwt에 대한 설명은 이전 포스트에 있다. JWT 모듈 등록 JWT 모듈을 사용하려면 다른 모듈에 import를 시켜야 한다. import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [JwtModule.register({})], }) export class AuthModule {} 위와 같이 등록하면 된다. JWT 모듈 메서드 JWT모듈 클래스에는 4개의 메서드가 있다. import { DynamicModule } from '@nestjs/common'; import { JwtModuleAsyncOptions, JwtModuleOptions } from './interfaces/jwt-module-options.interface'; export ..
2024.03.01
no image
[NestJS] 구글 로그인 (1)/구글 OAuth 적용
구글 OAuth 여기 에 구글 OAuth 사용 준비 방법이 나와있다. 해당 포스트를 따라 구글 OAuth 동의화면까지 완료하면 로직을 작성해야 한다. 구글 로그인 실행 순서 구글 OAuth를 사용해 구글에 사용자 정보를 요청한다 구글에서 이메일과 프로필 등 유저의 정보를 GoogleStrategy의 validate() 메서드에서 콜백으로 받는다. 이때 넘어오는 정보 중에 id 값을 확인 후 DB에 정보가 있는지 확인한다 정보가 있다면 JWT 토큰 발급, 없다면 DB에 정보 넣은 후 JWT토큰을 발급한다. Package 설치 npm i passport-google-oauth20 npm i -D @types/passport-google-oauth20 strategy 클래스 작성 nestjs에서 구글 로그인을..
2024.02.28
no image
[NestJS] Guard
가드(Guard) NestJS로 들어오는 요청은 컨트롤러단에 도달하기 전에 가드를 거쳐가도록 되어 있다. pipe 랑 비슷하지만 가드는 특정 상황들(permissions, roles, ACLs)에 따라서 request 가 라우터에 handling 될지 말지를 결정한다. 일반적으로 authorization 구현에 많이 쓰이며 Guard는 모든 middleware 이후에 실행되고, interceptor와 pipe 이전에 실행된다. canActivate 메서드 canActivate() 메서드는 가드의 핵심 로직을 구현하는 메서드이다. 해당 메서드는 executionContext 라는 파라미터를 받아서 요청의 context 정보를 알 수 있다. canActivate 메서드는 boolean 타입을 반환해야 하며,..
2024.02.22
no image
[NestJS] 커스텀 파이프
커스텀 파이프 파이프는 다양한 용도로 사용이 가능하다. 데이터 유효성 검사 데이터 변환 비즈니스 로직 처리 이에 따라서 커스텀 파이프도 다양하게 작성이 가능하다. 커스텀 파이프를 만들기 위해서는 필수로 PipeTransForm 인터페이스를 구현하는 클래스를 작성해야 한다. 또한 PipeTransForm 는 transform() 메소드를 정의한다. 이 transform() 메소드는 두 개의 파라미터를 가진다. 하나는 처리가 된 인자의 값이고, 나머지 한 개는 인자에 대한 메타 데이터를 포함한 객체다 export interface ArgumentMetadata { type: 'body' | 'query' | 'param' | 'custom'; metatype?: Type; data?: string; //@B..
2024.02.20

코루틴이란?

코루틴(Coroutine)은 Unity에서 사용되는 특수한 함수로, 일반 함수와 달리 실행을 중단하고 나중에 다시 재개할 수 있다. 코루틴은 IEnumerator 타입을 반환하고 yield 키워드를 사용하여 실행을 중단하고 특정 조건이 충족되면 다시 실행을 재개며, 이를 통해 시간 기반 작업이나 비동기 작업을 쉽게 처리할 수 있다.

 

다양한 yield 유형

 

지정된 시간(초) 동안 대기

yield return new WaitForSeconds(3f); // 3초 동안 대기

 

현재 프레임의 끝까지 대기, 다음 프레임에서 코루틴이 재개된다.

yield return null;

 

모든 렌더링 작업이 끝날 때까지 대기한다.

yield return new WaitForEndOfFrame();

 

다음 물리 업데이트가 될 때까지 대기한다.

yield return new WaitForFixedUpdate();

 

다른 코루틴이 완료될 때까지 대기한다.

yield return StartCoroutine(OtherCoroutine());

Image 태그에서 해당 경고 발생했다.

LCP 에  대한 경고이다.

 

Priority 속성

priority 속성은 Image 컴포넌트를 고우선 순위 리소스로 취급하며, 해당 이미지가 lazy loading 되지 않고 즉시 로딩되게 한다. 이 속성은 주로 화면의 상단에 표시되는 이미지에 유용하다.

 

 

<Image priority={true} alt="" src={"/images/Joy.png"} fill={true}/>

priority 를 true로 설정하면 경고 사라진다.

 

 

장점

priority 속성의 장점

  1. LCP(Largest Contentful Paint) 향상: 이미지가 즉시 로드되므로 LCP 지표가 개선다. LCP는 Core Web Vitals의 중요한 요소이다.
  2. 사용자 경험 개선: 중요한 이미지를 빠르게 로드함으로써 초기 페이지 로딩 시 사용자 경험이 향상됩니다.

 

 

Prisma 란?

Prisma는 자바스크립트와 타입스크립트에서 주목받고 있는 차세대 ORM 도구이다.

Prisma의 공식 홈페이지에선 자신을 이렇게 소개한다.

Next-generation Node.js and TypeScript ORM

 

Prisma는 Node.jsTypeScriptRustGo 등 다양한 언어와 프레임워크에서 사용할 수 있는 ORM (Object-Relational Mapping) 도구이다. 이는 개발자가 SQL 쿼리를 직접 작성하지 않고도 데이터베이스를 쉽게 다룰 수 있도록 도우며, 데이터베이스 초보자에게 더 쉽게 접근할 수 있게 도움을 주며, 능숙한 개발자들에게는 생산성을 높여준다.

 


 

Prisma의 작동

Prisma ORM를 사용하는 모든 프로젝트는 Prisma Schema 파일로 시작된다.  이 스키마 파일은 직관적인 데이터 모델링 언어로 애플리케이션 모델을 정의하고 데이터 소스에 대한 정보를 포함한다.

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User?   @relation(fields: [authorId], references: [id])
  authorId  Int?
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

 

해당 스키마에서는 세 가지 요소를 정의했다.

  • 환경변수를 통한 DB 커넥션
  • Prisma Client 생성
  • 모델 정의

 

 


Prisma 의 구성

Prisma는 크게 세 가지 부분으로 구성된다.

  • Prisma Client 
  • Prisma Migrate
  • Prisma  Studio

 

Prisma Client 

Prisma Client는 데이터베이스와 상호작용하는 데 사용되는 쿼리빌더이다. 이를 통해 안전하게 데이터베이스에 액세스를 가능하게 하며, 반복적인 코드를 줄여준다. Prisma Client를 사용하면 Sequelize, TypeORM과 같은 전통적인 ORM 또는 knex.js 와 같은 SQL 쿼리 빌더 대신 활용할 수 있다. Prisma Client는 Prisma의 일부이다.

  • Prisma 스키마와 모델을 기반으로 자동 생성된다.
  • 데이터베이스 테이블과 필드를 자동으로 매핑하여 쿼리를 생성한다
  • Prisma CLI를 통해 생성 및 업데이트할 수 있다.

 

 

Prisma Migrate

Prisma Migrate는 Prisma CLI를 통해 사용할 수 있는 데이터베이스 마이그레이션 도구이다. 이 도구는 Prisma Schema와 통합되어 데이터 모델링을 지원한다. 즉 데이터베이스에 새로운 테이블을 생성하거나, 기존 테이블에 열을 추가하는등의 작업을 하는 변경도구이다.

  • 데이터베이스 스키마를 Prisma Schema와 동기화한다.
  • 데이터베이스에 이미 존재하는 데이터를 보존한다.

아래의 명령어를 통해 DB에 반영하고 Prisma CLient를 업데이트할 수 있다.

npx prisma migrate save --experimental
npx prisma migrate up --experimental
npx prisma generate

 

 

 

Prisma  Studio

Prisma Studio 는 GUI다. 이 도구를 통해 스키마 탐색, 데이터 조회 및 수정 등의 작업을 할 수 있다.

 


 

그 외 Prisma Introspect도 있다. 이는 데이터베이스의 URL을 설정하고 

npx prisma introspect

해당 명령어를 통해 기존 데이터베이스의 구조를 Prisma Schema로 불러올 수 있다.

 

 

 

 

개인 기록용입니다. 만약 잘못된 부분이 있다면 댓글로 알려주신다면 감사하겠습니다!

JWT 발급

 

일반적으로는 이전 포스트까지 완료됐다면 DB에 해당 유저가 있는지 확인 후 없으면 DB에 저장, 있으면 JWT를 발급해서 로그인을 시켜야 한다. DB에 관련된 부분은 최대한 건너뛰고 JWT 관련된 것들만 작성하겠다.

 


 

userService 작성

 

JWT 발급 전 회원가입이 되어 있는지 확인하기 위해서 DB를 확인해야 한다. 이때 userService에 로직을 작성한다.

// user.service.ts
import { Injectable } from "@nestjs/common";

export interface GoogleUser {
  id: string;
  provider: string;
  name: string;
  email: string;
  photo: string;
}

@Injectable()
export class UserService {
  async findGoogleDataOrSave(user: GoogleUser) {
    return user;
  }
}

 

findGoogleDataOrSave 메서드에 DB에서 확인하고 저장하는 로직이 들어가 있어야 한다. 해당 로직은 생략 후 받아온 user 파라미터를 그대로 리턴한다.

 

 


 

JWT 발급 사전 준비

 

payload

JWT를 발급받기 위해서 payload가 필요하다.

// auth.controller.ts

export interface JwtPayload {
    iss: string;
    name: string;
    email: string;
    sub: string;
    }

// AuthController
@Get("google")
@UseGuards(GoogleAuthGuard)
async googleAuthRedirect(@Req() req: Request, @Res() res: Response) {
    const user = await this.userService.findGoogleDataOrSave(req.user as GoogleUser);
    const payload: JwtPayload = {
        iss: "JoY",
        name: user.name,
        email: user.email,
        sub: user.id,
    };
}

 

userService에서 받아온 user 데이터의 name, email, id 값과 , iss에는 발급자인 JoY를 페이로드에 넣는다 

 


 

JWT 모듈 import

jwt 모듈 import 방법은 이전 포스트에 있다.

expires와 secret_key 등은 accessToken과 refreshToken에 따라서 다르고, 따로 설정해야 하는 공용 옵션이 없기 때문에 register로 JWT 모듈을 등록하겠다.

// auth.module.ts
import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { GoogleStrategy } from "./google/auth.google.strategy";
import { UserModule } from "../user/user.module";
import { JwtModule } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";

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

 

 


 

JWT 발급

 

access token과 refresh token

access token은 보안을 위해 짧은 시간 동안만 유효하고, 만료되면 더 이상 사용이 불가능하다. access token을 통해서 서버에 api를 날려 데이터등을 요청할 수 있다.

 

refresh token은 새로운 access token을 발급하기 위해 필요한 토큰이다. access token과 함께 발급되고, access token이 만료되었을 때 새로운 access token와 refresh token을 발급해 준다. 또한 refresh token의 유효기간은 길어서 로그인 상태를 장기간 유지시켜 준다. refresh token으로는 서버에서 데이터 요청이 불가능하다.

refresh token은 DB에 저장하는 것이 가장 안전하고, 권장되는 방법이다. 

 

두 개의 토큰을 발급하는 이유는 access token의 유효기간이 짧아서 탈취되더라도 오랫동안 사용이 불가능하며 refresh token으로는 직접적으로 데이터에 접근이 불가능하다. 이로 인해 보안이 향상된다.

 


 

JWT 발급 로직

 

JWT를 발급해주는 로직은 auth.service.ts에 작성한다.

 

// auth.service.ts
import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { JwtPayload } from "./auth.controller";
import { ConfigService } from "@nestjs/config";

@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService, private readonly configService: ConfigService) {}

  getToken(payload: JwtPayload) {
    const accessToken = this.jwtService.sign(payload, {
      expiresIn: "1h",
      secret: this.configService.get("JWT_SECRET_ACCESS_TOKEN_KEY"),
    });

    const refreshToken = this.jwtService.sign(payload, {
      expiresIn: "7d",
      secret: this.configService.get("JWT_SECRET_REFRESH_TOKEN_KEY"),
    });

    return { accessToken, refreshToken };
  }
}

 

access token의 유효기간은 1시간, refresh token의 유효기안은 7일로 한다. 또한 env파일에서 시크릿키를 가져온다.

 

이어서 JWT 를 프론트 쿠키에 심어줘야 한다.

그전에 refresh token을 DB에 저장하는 메서드를 작성한다.

 

// user.service.ts

@Injectable()
export class UserService {
  //...
  async updateRefreshToken(refreshToken: string) {
    const isSuccess = true;
    return isSuccess;
  }
}

 

isSuccess는 DB에 저장이 성공했는지 판단한다.

 

//auth.controller.ts

enum CookieExpires {
  EXPIRES_IN_2HOURS = 3600000,
  EXPIRES_IN_7DAYS = 604800000,
}


@Controller("auth")
export class AuthController {
  //...
  
  @Get("google")
  @UseGuards(GoogleAuthGuard)
  async googleAuthRedirect(@Req() req: Request, @Res() res: Response) {
	
    //...
    
      const { accessToken, refreshToken } = this.authService.getToken(payload);

    const isSuccess = await this.userService.updateRefreshToken(refreshToken);

    if (isSuccess) {
      res.cookie("access_token", accessToken, {
        expires: new Date(Date.now() + CookieExpires.EXPIRES_IN_2HOURS), // 1시간 후 만료
        httpOnly: true, // 자바스크립트로 접근 불가
        secure: true, // HTTPS로만 전송
        sameSite: "strict", // 같은 사이트에서만 전송
      });
      res.cookie("refresh_token", refreshToken, {
        expires: new Date(Date.now() + CookieExpires.EXPIRES_IN_7DAYS), // 7일 후 만료
        httpOnly: true, // 자바스크립트로 접근 불가
        secure: true, // HTTPS로만 전송
        sameSite: "strict", // 같은 사이트에서만 전송
      });
      res.redirect("도메인");
    } else res.redirect(401, "localhost:3000");
  }

}

 

DB에 refresh token을 저장하고 성공하면 쿠키와 함께 페이지를 redirect 시키면 완료다. 만약 DB에 저장 실패 시 status 401과 함께 페이지만 redirect 시킨다.

 


 

 

 

개인 기록용입니다. 만약 잘못된 부분이 있다면 댓글로 알려주신다면 감사하겠습니다!

'프레임워크\라이브러리 > Nest.js' 카테고리의 다른 글

[NestJS] JWT 모듈  (1) 2024.03.01
[NestJS] 구글 로그인 (1)/구글 OAuth 적용  (1) 2024.02.28
[NestJS] Guard  (0) 2024.02.22
[NestJS] 커스텀 파이프  (0) 2024.02.20
[NestJS] 파이프  (1) 2024.02.18

JWT

jwt에 대한 설명은 이전 포스트에 있다.

 


 

JWT 모듈 등록

 

JWT 모듈을 사용하려면 다른 모듈에 import를 시켜야 한다.

import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [JwtModule.register({})],
})
export class AuthModule {}

 

위와 같이 등록하면 된다.

 


 

JWT 모듈 메서드

JWT모듈 클래스에는 4개의 메서드가 있다.

 

import { DynamicModule } from '@nestjs/common';
import { JwtModuleAsyncOptions, JwtModuleOptions } from './interfaces/jwt-module-options.interface';
export declare class JwtModule {
    static register(options: JwtModuleOptions): DynamicModule;
    static registerAsync(options: JwtModuleAsyncOptions): DynamicModule;
    private static createAsyncProviders;
    private static createAsyncOptionsProvider;
}

 

  • register : JwtModuleOptions 인터페이스에 정의된 옵션들을 받아서 JwtModule을 동기적으로 등록하는 메서드이다.
  • registerAsync : JwtModuleAsyncOptions 인터페이스에 정의된 옵션들을 받아서 JwtModule을 비동기적으로 등록하는 메서드이다.
  • createAsyncProviders : JwtModuleAsyncOptions 인터페이스에 정의된 옵션들을 받아서 JwtModule에 필요한 비동기적인 프로바이더들을 생성하는 메서드다. 해당 메서드는 내부적으로 사용되는 메서드이다. 외부에서 호출 불가.
  • createAsyncOptionsProvider : JwtModuleAsyncOptions 인터페이스에 정의된 옵션들을 받아서 JwtModuleOptions 프로바이더를 생성하는 메서드이다. 해당 메서드는 내부적으로 사용되는 메서드이다. 외부에서 호출 불가.

즉 JWT 모듈을 등록하는 방법은 register, registerAsync 두 가지 방법이 있다. register는 일반적인 등록 방법이고, registerAsync는 비동기적으로 option 을 가져와야 할 경우 사용한다.

 


 

JWT 모듈 register 옵션

JwtModule.register({}) 옵션에는 총 8가지가 있다.

  •  global : JwtModule을 전역 모듈로 등록할지 여부를 나타낸다. 전역 모듈로 등록하면 다른 모듈에서 재사용할 수 있다.
  • signOptions : JWT를 생성할 때 사용할 옵션들을 나타낸다. 알고리즘, 만료 시간 등등을 설정할 수 있다.
  • secret : JWT를 생성하고 검증할 때 사용할 비밀 키이다. 문자열이나 버퍼 형식이어야 한다.
  • publicKey : 비대칭 알고리즘을 사용하는 경우 JWT를 검증할 때 사용할 공개 키다. 문자열이나 버퍼 형식이어야 한다.
  • privateKey : 비대칭 알고리즘을 사용할 때 JWT를 생성할 때 사용할 개인 키다. 문자열이나 버퍼 형식이어야 한다.
  • secretOrPrivateKey : secret과 privateKey의 별칭이다. 이 속성은 deprecated 되었다.
  • secretOrKeyProvider : JWT를 생성하고 검증할 때 동적으로 비밀 키나 공개 키를 제공하는 함수를 나타낸다. 이 함수는 requestType, tokenOrPayload, options 세 개의 매개변수를 받고, 비동기적으로 jwt.Secret을 반환한다
  • verifyOptions : JWT를 검증할 때 사용할 옵션들을 나타낸다. 알고리즘 등을 설정 할 수 있다.

 

@Module({
  imports: [JwtModule.register({ secret: "secret_key", signOptions: { algorithm: "HS512" } })],
})

 


 

JWT 모듈 registerAsync 옵션

JwtModule.registerAsync({}) 옵션은 총 7가지가 있다.

  • global : JwtModule을 전역 모듈로 등록할지 여부를 나타낸다. register의 global 옵션과 같다.
  • useExisting : 기존의 서비스를 재사용하여 JwtModuleOptions을 제공하는 방식이다. JwtOptionsFactory 인터페이스를 구현해야 한다.
  • useClass : 새로운 클래스를 인스턴스화하여 JwtModuleOptions을 제공하는 방식이다. JwtOptionsFactory 인터페이스를 구현해야 한다.
  • useFactory : 팩토리 함수를 호출하여 JwtModuleOptions을 제공하는 방식이다. JwtModuleOptions을 동기적이나 비동기적으로 반환해야 한다.
  • inject : useFactory에 필요한 의존성들이다. 
  • extraProviders : JwtModule에 추가적인 프로바이더들을 나타낸다. 
  • imports : JwtModule에 필요한 다른 모듈들을 import 한다. 

 

import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";

@Module({
  imports: [
    JwtModule.registerAsync({
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>("JWT_SECRET_KEY"),
      }),
      inject: [ConfigService],
    }),
  ],
})



위와 같이 하면 추가적인 secret key를 설정할 필요가 없다.

 

 

 

개인 기록용입니다. 만약 잘못된 부분이 있다면 댓글로 알려주신다면 감사하겠습니다!

'프레임워크\라이브러리 > Nest.js' 카테고리의 다른 글

[NestJS] 구글 로그인 (2)/JWT 발급  (0) 2024.03.03
[NestJS] 구글 로그인 (1)/구글 OAuth 적용  (1) 2024.02.28
[NestJS] Guard  (0) 2024.02.22
[NestJS] 커스텀 파이프  (0) 2024.02.20
[NestJS] 파이프  (1) 2024.02.18

구글 OAuth

여기 에 구글 OAuth 사용 준비 방법이 나와있다. 해당 포스트를 따라 구글 OAuth 동의화면까지 완료하면 로직을 작성해야 한다.


 

구글 로그인 실행 순서

 

  1. 구글 OAuth를 사용해 구글에 사용자 정보를 요청한다
  2. 구글에서 이메일과 프로필 등 유저의 정보를 GoogleStrategy의 validate() 메서드에서 콜백으로 받는다.
  3. 이때 넘어오는 정보 중에 id 값을 확인 후 DB에 정보가 있는지 확인한다
  4. 정보가 있다면 JWT 토큰 발급, 없다면 DB에 정보 넣은 후 JWT토큰을 발급한다.

 

 

Package 설치

 

npm i passport-google-oauth20
npm i -D @types/passport-google-oauth20

 

 

strategy 클래스 작성

 

nestjs에서 구글 로그인을 만들기 위해선 GoogleStrategy 클래스를 정의해야 한다. 이 클래스는 PassportStrategy 클래스를 상속받아서 구글 인증에 필요한 설정을 하고, validate 메서드를 통해 구글에서 받은 토큰과 프로필 정보를 검증 및 처리한다.

 

import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-google-oauth20";

interface GoogleProfile {
  displayName: string;
  emails: { value: string }[];
  photos: { value: string }[];
  id: string;
  provider: string;
}

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly configService: ConfigService) {
    super({
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_SECRET_KEY,
      callbackURL: process.env.GOOGLE_CALLBACK_URL,
      scope: ["email", "profile"],
    });
  }
  
  // refreshToken를 얻기 위한 필수 코드
  authorizationParams(): {[key: string]: string; } {
    return ({
      access_type: 'offline',
      prompt: 'select_account',
    });
  }
  
  validate(accessToken: string, refreshToken: string, profile: GoogleProfile) {
    return {
      id: profile.id,
      provider:profile.provider,
      name: profile.displayName,
      email: profile.emails[0].value,
      photo: profile.photos[0].value,
    };
  }
}

 

옵션에 정의된 값들은 env 파일에 설정해 뒀다.

 

authorizationParams 메서드는 google에서 refresh token을 받기 위해서 필수로 작성해야 하는 메서드이다. 여기서 access_type을 "offline" 값을 리턴해야 한다.

이는 구글에 access token으로 api를 날릴 때 토큰이 만료되었을 경우 갱신해야 할 때 refresh token 이 필요한 경우에만 작성하면 된다.

 

validate 메서드는 Strategy를 통해 받아온 유저 정보를 해당 Strategy를 사용한 컨트롤러에 리턴해준다.

위의 로직에서는 name, email, photo, id, provider 다섯 가지만 return 하는데 profile을 console에 찍어보고 필요한 부분은 추가해서 return 하면 된다. 여기서 id 와 email, provider 은 DB에 확인하기 위해서 저장해둔다. provider로 어느 소셜로그인인지 확인, id는 구글에서 가입을 한적이 있는지 확인, email은 다른 소셜로그인에서 중복된 email을 사용했는지 확인하기 위해서 저장한다.


 

GoogleAuthGuard  작성

import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class GoogleAuthGuard extends AuthGuard("google") {
  async canActivate(context: any): Promise<boolean> {
    const result = (await super.canActivate(context)) as boolean;
    return result;
  }
}

controller에 사용할 guard 를 작성한다.

 


 

Controller 작성

컨트롤러에는 두 개의 라우터를 작성해야 한다

  • 구글 로그인 페이지로 redirection 할 라우터
  • 구글 로그인 후 callback URL로 오는 요청을 처리할 라우터

 

import { GoogleAuthGuard } from "./google/auth.google.guard";
import { Controller, Get, Query, Req, Res, UseGuards } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { Request, Response } from "express";

@Controller("auth")
export class AuthController {
  constructor(
    private readonly authService: AuthService
  ) {}

  @Get("google-login")
  @UseGuards(GoogleAuthGuard)
  async googleAuth(@Req() req: Request) {}

  @Get("google")  
  @UseGuards(GoogleAuthGuard)
  async googleAuthRedirect(@Req() req: Request, @Res() res: Response) {
    const { user } = req;
    return user;
  }
}

google-login 라우터와 콜백 라우터인 google을 만들었다. 

 

콜백 라우터에서 user를 콘솔에 찍어보면 해당 데이터가 잘 나온다

 

 

 

여기까지 코드의 실행순서는 이러하다.

  1. 클라이언트가 auth/google-login 에 GET 요청을 보내고, 해당 라우터가 실행된다
  2. @UseGuards(GoogleAuthGuard) 데코레이터에 의해 GoogleAuthGuard 가 호출된다.
  3. GoogleAuthGuard 클래스의 canActivate 메서드에 의해서 부모 클래스의 canActivate 메서드를 실행한다.
  4. 부모 클래스인 AuthGuard는 Passport의 인증 메서드를 호출하여 인증을 시도한다
  5. 인증 성공 후 canActivate 메서드는 true를 반환한다.
  6. 인증이 성공된 경우 클라이언트는 Google의 OAuth 서비스에 리디렉션 되어 로그인을 시도하게 된다.
  7. 로그인 완료하면 Passport의 콜백 함수가 호출되며 작성한 GoogleStrategy 클래스의 validate 메서드가 호출된다.
  8. 추출된 유저 정보를 콜백라우터에 전달한다.

 


 

모듈에 등록

GoogleStrategy 와 controller는 module에 등록해야 한다.

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { GoogleStrategy } from './google/auth.google.strategy';

@Module({
  controllers: [AuthController],
  providers: [AuthService, GoogleStrategy],
})
export class AuthModule {}

 


 

 

 

 

개인 기록용입니다. 만약 잘못된 부분이 있다면 댓글로 알려주신다면 감사하겠습니다!

'프레임워크\라이브러리 > Nest.js' 카테고리의 다른 글

[NestJS] 구글 로그인 (2)/JWT 발급  (0) 2024.03.03
[NestJS] JWT 모듈  (1) 2024.03.01
[NestJS] Guard  (0) 2024.02.22
[NestJS] 커스텀 파이프  (0) 2024.02.20
[NestJS] 파이프  (1) 2024.02.18

가드(Guard)

NestJS로 들어오는 요청은 컨트롤러단에 도달하기 전에 가드를 거쳐가도록 되어 있다. pipe 랑 비슷하지만 가드는 특정 상황들(permissions, roles, ACLs)에 따라서 request 가 라우터에 handling 될지 말지를 결정한다. 일반적으로 authorization 구현에 많이 쓰이며 Guard는 모든 middleware 이후에 실행되고, interceptor와 pipe 이전에 실행된다.

 


 

canActivate 메서드

canActivate() 메서드는 가드의 핵심 로직을 구현하는 메서드이다. 해당 메서드는 executionContext 라는 파라미터를 받아서 요청의 context 정보를 알 수 있다. 

canActivate 메서드는 boolean 타입을 반환해야 하며, true 이면 요청이 허용되고, false 면 요청이 거부된다.

canActivate 메서드에 사용자의 권한이나 역할등을 확인하는 로직을 추가 할 수 있다. 

 


 

글로벌 가드 설정

X-API-KEY 를 발급해서 프론트에서 오는 요청에 X-API-KEY가 없는 경우 Guard에서 막도록 로직을 구현했다. X-API-KEY는 모든 라우터에서 검사해야 하기 때문에 전역가드로 설정해 뒀다.

가드 생성

import { BadRequestException, CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";

@Injectable()
export class JoyGuard implements CanActivate {
  canActivate(ctx: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const req = ctx.switchToHttp().getRequest();
    if (req.headers["x-api-key"] !== process.env.X_API_KEY)
      throw new BadRequestException({ statusCode: 401, message: "unauthorized" });
    return true;
  }
}

위와 같이 가드를 만들고 헤더에 x-api-key 가 env 파일에 넣어둔 X-API-KEY 와 다른 경우 unauthorized 에러를 뱉게 했다. env 파일 설정은 여기를 보면 된다.

 


 

가드 추가

모든 라우터에서 해당 가드를 통과하게 글로벌 가드를 설정했다.

//main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { BadRequestException, ValidationPipe } from "@nestjs/common";
import * as cookieParser from "cookie-parser";
import { JoyGuard } from "./joy/joy.guard";
declare const module: any;

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

  app.useGlobalGuards(new JoyGuard()); //가드 추가

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

 

 


 

요청 결과

X-API-KEY 가 다를 경우 아래와 같이 에러가 난다.

guard 를 통과하지 못할 경우 로직은 실행되지 않으며 불필요한 자원의 낭비를 막는다.

 

 

 

 

개인 기록용입니다. 만약 잘못된 부분이 있다면 댓글로 알려주신다면 감사하겠습니다!

'프레임워크\라이브러리 > Nest.js' 카테고리의 다른 글

[NestJS] JWT 모듈  (1) 2024.03.01
[NestJS] 구글 로그인 (1)/구글 OAuth 적용  (1) 2024.02.28
[NestJS] 커스텀 파이프  (0) 2024.02.20
[NestJS] 파이프  (1) 2024.02.18
[NestJS] 환경 변수  (0) 2024.02.16

커스텀 파이프

파이프는 다양한 용도로 사용이 가능하다.

  • 데이터 유효성 검사
  • 데이터 변환
  • 비즈니스 로직 처리

이에 따라서 커스텀 파이프도 다양하게 작성이 가능하다.

커스텀 파이프를 만들기 위해서는 필수로 PipeTransForm 인터페이스를 구현하는 클래스를 작성해야 한다. 또한 PipeTransForm transform() 메소드를 정의한다.

이 transform() 메소드는 두 개의 파라미터를 가진다. 하나는 처리가 된 인자의 값이고, 나머지 한 개는 인자에 대한 메타 데이터를 포함한 객체다

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;  
  data?: string; //@Body('string') 데코레이터에 전달된 문자열
}

 


컨트롤러 레벨, 핸들러 레벨

컨트롤러 레벨과 핸들러 레벨 파이프는 완전 똑같다고 보면 된다. 

import { ArgumentMetadata, Injectable, PipeTransform } from "@nestjs/common";

@Injectable()
export class CustomJoyPipe1 implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    console.log(111, value);
    console.log(222, metadata);
    return value;
  }
}
import { Controller, Get, Param, Query, DefaultValuePipe, ParseIntPipe, UsePipes, Body } from "@nestjs/common";
import { JoyService } from "./joy.service";
import { JoyDto } from "./dto/joy.dto";
import { CustomJoyPipe1 } from "./joy.pipe";

@Controller("joy")
export class JoyController {
  constructor(private readonly joyService: JoyService) {}

  @Get("customPipe/:id")
  @UsePipes(CustomJoyPipe1)
  getCustomPipe(@Body() body: any, @Query() query: any, @Param() param: any) { //arguments 의 역순으로 실행된다
    console.log("body", body);
    console.log("query", query);
    console.log("param", param);
    return body;
  }
}

여기서 또 다른 사실을 알게 됐다. 핸들러레벨 파이프와 컨트롤러 레벨 파이프는 한번 동작하는 게 아닌 body, param, query 등에 따라서 여러 번 동작한다. 또한 메소드에 전달된 arguments 의 역순으로 실행된다. 즉   getCustomPipe(@Body() body: any, @Query() query: any, @Param() param: any)  인 경우 param, query, body 순으로 pipe 로직이 실행된다.


파라미터 레벨

파라미터 레벨 파이프는 조금은 다르다. meyatype, type, 은 같지만, data 에 해당 파라미터의 값이 들어가 있다.

import { Controller, Get, Param, Query, DefaultValuePipe, ParseIntPipe, UsePipes, Body } from "@nestjs/common";
import { JoyService } from "./joy.service";
import { JoyDto } from "./dto/joy.dto";
import { CustomJoyPipe1, CustomJoyPipe2 } from "./joy.pipe";

@Controller("joy")
@UsePipes(CustomJoyPipe1)
export class JoyController {
  constructor(private readonly joyService: JoyService) {}

  @Get("customPipe/:id")
  @UsePipes(CustomJoyPipe1)
  getCustomPipe(
    @Body() body: any,
    @Body("email", CustomJoyPipe2) email: string,
    @Param() param: any,
    @Query() query: any
  ) {
    console.log("body", body);
    console.log("query", query);
    console.log("param", param);
    return body;
  }

}
@Injectable()
export class CustomJoyPipe2 implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    console.log(333, value);
    console.log(444, metadata);
    return value;
  }
}

이렇게 파이프를 넣은 파라미터의 이름이 data 에 들어가 있다.


커스텀 파이프 작성

parseIntPipe 를 구현해 보자.

@Injectable()
export class CustomParseIntPipe implements PipeTransform {
  transform(value: string, metadata: ArgumentMetadata) {
    const val = parseInt(value);// 형변환
    if (isNaN(val)) throw new BadRequestException(`${metadata.data} must be a number`);
    return value;
  }
}

 value 를 parseInt 해서 숫자가 아니면 에러가 난다.

 

 

 

개인 기록용입니다. 만약 잘못된 부분이 있다면 댓글로 알려주신다면 감사하겠습니다!

'프레임워크\라이브러리 > Nest.js' 카테고리의 다른 글

[NestJS] 구글 로그인 (1)/구글 OAuth 적용  (1) 2024.02.28
[NestJS] Guard  (0) 2024.02.22
[NestJS] 파이프  (1) 2024.02.18
[NestJS] 환경 변수  (0) 2024.02.16
[NestJS] DTO  (1) 2024.02.12