[Week13] NestJS Ch4 프로바이더

안나경·2024년 4월 12일

크래프톤정글

목록 보기
54/57

4.1 프로바이더

서버가 제공하는 핵심 기능은
전달 받은 데이터를 어떻게 비즈니스 로직으로 해결하는가.
(이런걸 비즈니스 로직이라고 하나보군.)

앱이 제공하고자 하는 핵심 기능,
즉 비즈니스 로직을 수행하는 역할을 프로바이더, provider라고 한다.

단일 책임 원칙, single responsibility principle, SRP에
부합하게 하려면 따로 모듈화 하는게 좋겠지!

프로바이더는
서비스, 저장소(repository), 팩터리(Factory), 헬퍼(helper)등
여러가지 형태로 구현이 가능 하다.

Nest에서 제공하는 프로바이더의 핵심은
의존성을 주입할 수 있다는 점.

컨트롤러 구조를 보면

Controller-constructor로 UserService 생성
->UserService.remove 등 생성자로 생성한 객체 내 method로 비즈니스 로직 수행.

으로 이루어져있다.

만약 @Injectable() 데커레이터를 쓴다면

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
	...
    remove(id: number) {
    	return 'This action removes a #${id} user';
        }
    }

식으로, class를 선언하는 ts에서
이 데커레이터를 선언하여

다른 어떤 Nest 컴포넌트에서도 주입할 수 있는 프로바이더가 된다.
별도의 스코프를 지정해주지 않으면 일반적으로 싱글턴 인스턴스가 생성된다.

싱글턴 인스턴스는
일단 하나를 생성하고
그것 자체를 지들끼리 공유한다는 말인거같다.

어디서든 주입할수 있다는건 어디서든 호출이 가능하다는 말 같고.

4.2 프로바이더 등록과 사용

4.2.1 프로바이더 등록

프로바이더 인스턴스 역시
모듈에서 사용할수 있도록 등록해줘야한다.

Module 파일 안에 provider 카테고리안에 들어있을 것.

(확실히
users 폴더 안에 UserModule이 있고
users/entitis 라는 폴더 안에 @Injectable() 선언된 UserService가 있음.)

(근데 Appmodule 이라는게 따로 있어서,
각자 연결되어있는 영역과 범위를 따로 두나보군.)

4.2.2 속성 기반 주입

음.
생성자 기반으로 하면
Base 안에
service A, service B를 달고 싶고.
service B가 base 기반으로 추가 기능을 쓰되
service A의 속성을 쓰고 싶다면.

  • service B 단에서 service A method를 쓰기 위해 constructor할 때
    단순히 선언만 하는게 아니라 내부에 super(_serviceA) 등을 선언해줘야한다.
  • 그렇지만 그게 번거로우니, 애초에 Base에서 Service A를 생성해서 쓸때 생성자 기반으로 주입받지 않고 속성 기반으로 주입을 받는다.
    1) constructor(private readonly _serviceA:ServiceA){super(_surviceA)
    2) @Inject(ServiceA) private readonly serviceA: ServiceA;

이 데커레이터는 인수로 타입(클래스 이름), 문자열, 심벌이 가능하지만
대개 @Injectable 이 선언된 클래스는 클래스 이름 타입이면 됨.

4.3 유저 서비스에 회원가입 로직 구현하기

  • 회원 가입 화면에서 유저 정보를 입력 받아 유저 생성 요청 받기.
  • DB에 유저 정보를 저장하고 유저에게 회원 가입 확인 이메일 발송.
    (DB 저장 로직은 8장에서 TypeORM 학습후 구현.)
  • 이메일 본문에는 이메일 검증을 위한 요청으로의 링크를 포함,
    사용자가 링크를 누르면 이메일 승인 요청이 들어옴.
    회원 가입 준비 단계에 있는 유저를 승인.

4.3.1 UserService 프로바이더 생성

nest g s Users 명령어로 UserService 프로바이더 생성.

이후에...
쭉 뼈대 만들고
채우는 걸로 구성했길래

돌아가나 끝까지 한번 해봄.

후...
하드 코딩하라는 말을 잘 못알아들어서
500 에러 뜨면서 우와 500에러다 싶지만
다 알려주는데 500에러??? 하면서 함

아무튼

  • 새로운 서비스를 추가하고 싶으면 s 명령어로 생성하기.

4.3.2 회원가입

app.controller

import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UserLoginDto } from './dto/user-login.dto';
import { VerifyEmailDto } from './dto/verify-email.dto';
import { UserInfo } from './UserInfo';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Post()
  async createUser(@Body() dto: CreateUserDto): Promise<void> {
    console.log(dto);
    const { name, email, password } = dto;
    await this.usersService.createUser(name, email, password);
  }

constructor로 프로바이더 private하게 만들어서
본래 생성하는 CreateUser 함수를...
dto에서 가져와 변수로 선언하고

프로바이더 내 메소드로 연결하고 있음.

이메일 검증시 필요한 토큰 형식을 uuid로 한다고 함.
npm i uuid
npm i --save-dev @types/uuid

로 함.

그리고 UsersService를
폴더 내 파일에서 구현하는데 동작이

  • checkUserExists
    이미 존재하는지 확인하고 존재한다면 에러.
  • saveUser
    유저를 데이터베이스에 저장.
    토큰을 동봉하는데, 회원가입 메일을 눌러 링크에서 인증이 되면서 받게 되는 토큰.
    (이때서야 이 토큰을 받을 수 있으니 검증이 되겠지.)
    (토큰도 유효기간 설정하여 생성.)
  • sendMemberJoinEmail
    회원 가입 인증 이메일 발송.
import * as uuid from 'uuid';
import { Injectable } from '@nestjs/common';
import { EmailService } from 'src/email/email.service';

@Injectable()
export class UsersService {
  async createUser(name: string, email: string, password: string) {
    await this.checkUserExists(email);

    const signupVerifyToken = uuid.v1();

    await this.saveUser(name, email, password, signupVerifyToken);
    await this.sendMemberJoinEmail(email, signupVerifyToken);
  }

  private checkUserExists(email: string) {
    return false; //TODO: DB 연동후 구현
  }

  private saveUser(
    name: string,
    email: string,
    password: string,
    signupVerifyToken: string,
  ) {
    return; // TODO: DB 연동후 구현.
  }

  private async sendMemberJoinEmail(email: string, signupVerifyToken: string) {
    await this.emailService.sendMemberJoinVerification(
      email,
      signupVerifyToken,
    );
  }
}

4.3.3 회원가입 이메일 발송

외부 이메일 서비스 고를시 고려할 점

  • 이메일 전송
  • 전송 기록 확인
  • 이메일 보안
  • 스팸 처리
  • 바운스 확인 기능 제공 여부

등이 있지만 가장 중요한건 이메일 전송 자체의 안정성.

무료로 이메일 전송을 해주는 nodemailer을 써볼 예정.

npm i nodemailer
npm i @types/nodemailer --save-dev

위에 UsersService는 유저 정보 저장, 조회를 하지만,
Email 처리는 EmailService 프로바이더로 관리해보자.

nest g s Email 로 프로바이더 폴더를 생성하고,
(spec은 테스트 코드로 사용하라고 줌.)

UsersService에서 sendMemberJoinEmail을 구현해야하니

  • UsersService 안에
    constructor로 emailService를 생성하여 주입하기.
    우리가 만들었던 EmailService 파일을 import해서 선언하자.
import { EmailService } from 'src/email/email.service';

@Injectable()
export class UsersService {
  constructor(private emailService: EmailService) {}
...
  private async sendMemberJoinEmail(email: string, signupVerifyToken: string) {
    await this.emailService.sendMemberJoinVerification(
      email,
      signupVerifyToken,
    );
  }
}

그리고 EmailService 에서
nodemailer로 이메일을 전송하는 걸 구현해보자.

  • 메일 옵션 타입.
    수신자(to), 메일 제목(subject), html형식 본문(html)
  • nodemailer에서 제공하는 Transporter 객체 생성.
  • 유저가 누를 버튼이 가질 링크 구성.
  • 메일 본문을 구성. form 태그로 POST 요청 유도.
  • transporter 객체를 이용하여 메일 전송.
import Mail = require('nodemailer/lib/mailer');
import * as nodemailer from 'nodemailer';
import { Injectable } from '@nestjs/common';

interface EmailOptions {
  to: string;
  subject: string;
  html: string;
}

@Injectable()
export class EmailService {
  private transporter: Mail;

  constructor() {
    this.transporter = nodemailer.createTransport({
      service: 'Gmail',
      auth: {
        user: '하드코딩해야하는 이메일.',
        pass: '하드코딩해야하는 비번.',
      },
    });
  }

  async sendMemberJoinVerification(
    emailAddress: string,
    signupVerifyToken: string,
  ) {
    const baseUrl = 'http://localhost:3000';

    const url = `${baseUrl}/users/email-verify?signupVerifyToken=${signupVerifyToken}`;

    const mailOptions: EmailOptions = {
      to: emailAddress,
      subject: '가입 인증 메일',
      html: `
        가입 확인 버튼을 누르시면 가입 인증이 완료됩니다.<br/>
        <form action="${url}" method="POST>
         <button>가입확인</button>
         </form>
         `,
    };
    return await this.transporter.sendMail(mailOptions);
  }
}

이후 장에는 하드코딩하지 않고 환경변수로 관리할 것.

흠..
nodemailer에서 Mail이라는 인터페이스를 가져와서
멤버 변수, transporter를 만들어주었으니
그 안을 nodemailer method로 생성하고

함수에서 받은 매개변수에 따라
url을 구성하는데 쿼리 형식에 따라 채워서

메일 본문은 mailOptions라는 변수로
내가 설정했던 MailOptions라는 interface에 따라
html로 보내주는군....

아마 지금은 curl 할때 형식에 맞춰 보내긴 하지만
실제로 내가 보낸 인수를 쓰고 있진 않을 거임.

잘 도착하는거 확인했음.

curl http://localhost:3000/users -H "Content-Type: application/json" -X POST -d '{"name":"YOUR_NAME", "email":"하드코딩한 이메일", "password":"하드코딩한 비밀번호"}'

4.3.4 이메일 인증

4.3.5 로그인

4.3.6 유저 정보 조회

profile
개발자 희망...

0개의 댓글