관리 메뉴

bright jazz music

[nestjs] 2.1. auth구현 (1) : 회원가입,로그인,로그아웃 본문

Framework/NestJS

[nestjs] 2.1. auth구현 (1) : 회원가입,로그인,로그아웃

bright jazz music 2024. 12. 19. 18:03

1. CRUD 제너레이터로 CRUD 파일 생성

아래 방법을 사용하는 이유는 공식적으로 권장하는 방식을 이해하고 이를 참고하여 auth 기능을 만들어 내기 위함이다.

굳이 참고할 필요 없으면 바로 2번 auth기능 구현으로 건너 뛰어도 된다.

 

https://docs.nestjs.com/recipes/crud-generator

 

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

 

 

 

 

//터미널에서

nest g resource
? What name would you like to use for this resource (plural, e.g., "users")? users
? What transport layer do you use? (Use arrow keys)
❯ REST API 
  GraphQL (code first) 
  GraphQL (schema first) 
  Microservice (non-HTTP) 
  WebSockets 
  
  //----
  
  ? What name would you like to use for this resource (plural, e.g., "users")? users
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/users/users.controller.spec.ts (566 bytes)
CREATE src/users/users.controller.ts (894 bytes)
CREATE src/users/users.module.ts (248 bytes)
CREATE src/users/users.service.spec.ts (453 bytes)
CREATE src/users/users.service.ts (609 bytes)
CREATE src/users/dto/create-user.dto.ts (30 bytes)
CREATE src/users/dto/update-user.dto.ts (169 bytes)
CREATE src/users/entities/user.entity.ts (21 bytes)
UPDATE package.json (1984 bytes)
UPDATE src/app.module.ts (312 bytes)
✔ Packages installed successfully.

 

 

 

위처럼 src/users라는 디렉토리가 생기고, 유저 생성/수정과 관련된 파일들이 생성되었다.

 

src/
└── users/
    ├── entities/
    │   └── user.entity.ts        # 데이터베이스 엔티티
    ├── dto/
    │   ├── create-user.dto.ts
    │   └── update-user.dto.ts
    ├── users.controller.ts       # 컨트롤러
    ├── users.service.ts          # 서비스
    ├── users.module.ts           # 모듈
    └── users.repository.ts       # (선택적) 커스텀 레포지토리

일반적인 모듈구조.

 

이 파일구성을 참고해서 로그인, 로그아웃과 관련된 auth 기능을 추가한다.

 

2. auth 구성

 

auth에는 회원가입, 로그인, 로그아웃 기능이 들어간다. 이 때 소셜 회원가입과 소셜 로그인도 가능해야 한다. 리프레쉬 토큰을 사용하여 갱신하는 갱신 기능은 추후 추가한다.

src/
└── auth/
    ├── dto/
    │   ├── login.dto.ts
    │   └── social-register.dto.ts
    ├── auth.controller.ts
    ├── auth.service.ts
    └── auth.module.ts
    
// 독립적으로 저장할 정보가 없으므로 엔티티는 만들지 않았다.
/*
엔티티(Entity)의 역할은:

데이터베이스 테이블의 구조를 정의
테이블에 저장될 데이터의 형태를 클래스로 표현
TypeORM이 이 클래스를 보고 실제 DB 테이블을 생성

독립적인 데이터란:

그 자체로 저장되고 관리되어야 하는 데이터
다른 엔티티와 독립적으로 존재할 수 있는 데이터
예를 들어

// User 엔티티 - 독립적인 데이터 O
@Entity()
class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;
  // 이 데이터는 DB에 저장되어야 함
}

그런데

// Auth - 독립적인 데이터 X
class Auth {
  token: string;
  // 이건 로그인 시 생성되는 임시 데이터
  // DB에 저장할 필요 없음
}
*/

/*
auth가 하는 일은:

로그인/회원가입 처리
JWT 토큰 생성/검증
인증된 사용자 정보를 User 엔티티에서 조회

이런 작업들이다. 이 과정에서 필요한 데이터는:

사용자 정보 → User 엔티티에 저장
토큰 정보 → JWT로 생성되고 클라이언트에 전달
로그인 시도 → 일시적인 처리일 뿐 저장할 필요 없음

굳이 auth 관련 데이터를 저장하고 싶다면 (예: 로그인 이력),
그건 User 엔티티에 lastLoginAt 같은 필드를 추가하거나,
별도의 LoginHistory 엔티티를 만드는 게 더 적절
*/

 

우선은 위와 같이 파일을 먼저 생성했다.

 

 

dto구성부터 보자

 

2.1. DTO 구성

 

2.1.1 LocalRegisterDto

 

소셜 회원가입을 사용하지 않고 사용자가 직접 이메일과 비밀번호를 넣어 가입하는 경우에 사용하는 dto이다.

 

(참고로 노드에서는 보통 파일명은 케밥-케이스(kebab-case)나 점을 사용하고, 클래스명은 파스칼케이스(PascalCase)를 사용한다. nestjs에서 자동으로 파일을 만들어주는 경우에도 이 규칙을 따른다. 따라서 파일명은 xxx-xxx.xxx.ts이다. 하지만 그러한 이름의 파일 내에서의 클래스 작성 시에는 자바와 같이 파스칼 케이스를 사용하는 것을 볼 수 있다)

// local-register.dto.ts 
//경로: src/auth/dto/local-register.dto.ts

import { IsEmail, IsString, MinLength } from 'class-validator';
// 클래스 검증 데코레이터 (pnpm add class-validator class-transformer)
// NestJS 앱에서 이 검증을 활성화하려면 main.ts에서 ValidationPipe를 추가해야 한다.

// 로컬 회원가입 시 사용되는 DTO(사용자가 직접 이메일과 비밀번호를 입력하는 경우)
export class LocalRegisterDto {
  @IsEmail()
  email: string;
  
  @IsString()
  @MinLength(8)
  password: string;
}

 

이 파일에서는 class-validator를 사용한다. 기본 nest라이브러리에는 들어있지 않으므로 아래와 같이 설치해 준다.

pnpm add class-validator class-transformer

//(npm을 사용한다면 npm install class-validator class-transformer사용. yarn이라면 pnpm부분을 yarn으로만 교체해주면 됨)

 

설치하고 나서 사용해야 하는 속성 위에 데코레이터를 달아주면 된다. @IsMail(), @IsString() 등.

만약 해당 데코레이션에 맞지 않는 데이터가 들어오는 경우 자동적으로 400에러를 반환한다.

 

하지만 이 기능을 유효하게 하려면 먼저 src/main.ts에서 ValidationPipe를 추가해 주어야 한다.

// main.ts:스프링부트의 @SpringBootApplication가 붙어있는 파일(메인함수가 있는 파일)에 대응되는 파일

// 유효성 검사 파이프 임포트
import { ValidationPipe } from '@nestjs/common';


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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe()); // 전역 유효성 검사 파이프 추가 **
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

 

이제 프로젝트를 구동하고 포스트맨과 같은 http 클라이언트로 요청을 보내보자. 핸들러가  POST 메서드를 사용하고 있으므로 POST로 보내야 제대로 에러 메시지를 볼 수 있다. 

 

- 이메일이 이메일 형식이 아니며,

- 비밀번호가 8자보다 짧고,

- 비밀번호가 문자열이 아니라는 메시지가 message배열에 담겨 온다.

 

앞서 언급했듯이 statusCode는 400이 수신된다. (Bad  Request)

 

물론 class-validator를 사용하지 않아도 된다. 하지만 그럴 경우 검증처리를 아래처럼 일일이 해줘야 한다.

@Post('login')
async login(@Body() loginDto: LoginDto) {
  // 수동으로 이메일 형식 체크
  if (!isValidEmail(loginDto.email)) throw new BadRequestException();
  
  // 수동으로 비밀번호 길이 체크
  if (loginDto.password.length < 8) throw new BadRequestException();
  
  // 로그인 로직...
}

 

 

2.1.2 LocalLoginDto

 

사용자가 소셜 로그인을 하지 않고 직접 이메일과 비밀번호를 입력해 로그인 할 때 사용하는 dto

// local-login.dto.ts
//경로: src/auth/dto/local-login.dto.ts

import { IsEmail, IsString, MinLength } from 'class-validator';

export class LocalLoginDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}

 

 

2.1.3 SocialRegisterDto

 

사용자가 소셜 회원가입(구글, 카카오 등)을 이용해서 회원가입하는 경우 사용하는 dto

// social-register.dto.ts
// 경로: src/auth/dto/social-register.dto.ts

import { AuthProvider } from '@common/enums/auth-provider.enum';
import { IsString, IsEnum } from 'class-validator';

// 소셜 회원가입 시 사용되는 DTO
export class SocialRegisterDto {
    @IsString()
    token: string;  // 소셜 로그인 후 받은 액세스 토큰
  
    @IsEnum(AuthProvider)
    provider: AuthProvider;  // 소셜 서비스 구분자 (GOOGLE, KAKAO 등)
}

/*
소셜 회원가입 프로세스:
1. 사용자가 소셜 회원가입 버튼 클릭
2. 소셜 인증 완료 후 프론트엔드가 액세스 토큰을 받음
3. 프론트엔드가 토큰과 provider 정보를 서버로 전송
4. 서버는 받은 토큰을 사용해 해당 소셜 서비스의 API 호출
5. 소셜 API에서 사용자의 고유 ID를 받아옴
    (해당 서비스에서의 사용자의 동의에 따라 고유 아이디 말고도 이름, 이메일 등 정보를 받아올 수 있음
    그러나 기본적으로 고유 ID만 받아오는 것이 일반적)
6. 해당 소셜 ID로 새 계정을 생성하고 JWT 토큰 발급
*/

 

AuthProvider가 없어서 에러가 발생할 것이므로 common 디렉토리를 만들고 파일을 추가해주자.

// auth-provider.enum.ts
// 경로: src/common/enums/auth-provider.enum.ts

export enum AuthProvider {
  LOCAL = 'local',
  GOOGLE = 'google',
  KAKAO = 'kakao',
  NAVER = 'naver'
}

/*
enum을 사용하면 타입 안정성(Type Safety)을 확보하고 IDE의 자동완성 기능을 활용할 수 있어, 
개발 과정에서의 실수를 방지하고 생산성을 높일 수 있다.
또한 데이터베이스 레벨에서도 enum 타입으로 컬럼을 정의하면 허용된 값만 저장되도록 제약을 걸 수 있다
*/

 

위 주석에서 적어놓은 것처럼 프론트엔드에서 소셜 로그인을 하면 소셜 서비스에서는 프론트엔드로 토큰을 보낸다. 해당 서비스에서 AuthProvider를 보내는 것이 아니다. 프론트엔드에서 소셜 서비스를 구분하여 백엔드로 제이슨을 만들어 보내는 것이다. 이는 nestjs api서버가 AuthProvider를 구분하여 소셜 서비스에 토큰을 보내 정보를 받아오기 위함이다.

 

// 예를 들어 이렇게 사용....
// auth.service.ts
async validateSocialUser(provider: AuthProvider, token: string) {
  switch (provider) {
    case AuthProvider.GOOGLE:
      return this.validateGoogleToken(token);
    case AuthProvider.KAKAO:
      return this.validateKakaoToken(token);
    // ...
  }
}

 

 

2.1.4 SocialLoginDto

 

사용자가 소셜 로그인을 사용할 때 사용하는 dto

 

// social-login.dto.ts
// 경로: src/auth/dto/social-login.dto.ts

import { AuthProvider } from '@common/enums/auth-provider.enum';

export class SocialLoginDto {
  provider: AuthProvider;
  token: string; // 프론트엔드에서 받은 토큰
}
/*
소셜 로그인 프로세스

- 사용자가 소셜 로그인 클릭
- 소셜 로그인 완료 후 프론트엔드가 token 받음
- 그 token을 서버로 전송
- 서버가 token으로 소셜 API 호출해서 socialId 획득
- socialId로 DB에서 사용자 찾아서 로그인 처리
*/

 

Comments