개발/TypeScript

🫧TS:: NodeJS + Express로 서버 만들기

hyuunii 2023. 1. 13. 15:22

TS?

아주 아주 간단히 말하자면 JS라는 언어에 에디터 부가 기능을 추가한 느낌이랄까. JS의 상위 호환 버젼이라고 보면 된다.

TS의 가장 큰 특징은 JS를 기본으로 거기에 변수 만들때 타입 지정해줄 수 있다는 것이다. 또한 class, interface 등을 추가해서 자바나 C#과 같은 OOP 프로그래밍을 할 수 있다.

TS로 작성한 코드는 최종적으로 JS로 컴파일되어 실행되는데, 타입을 지정해주는 TS의 특성상 JS와는 다르게 타입 에러 등의 에러들이 코드 실행 '전'에 잡힌다. 이런 장점으로 JS의 전체 에러 15%가량이 TS를 사용함으로써 해결될 수 있다고 한다.

 

 

서버 만들기(회원가입 및 로그인까지~!)

1. 초기 설정

npm i

//Node.js 상에서 TypeScript Compiler를 통하지 않고도, 직접 TypeScript를 실행시킬 수 있게 함.
npm i --save-dev ts-node typescript

//Exress 설치
npm i express

//Express 모듈에 대한 type을 지원
npm i @types/express --dev

//Node.js 타입을 추가
npm i @types/node --dev

//tsconfig.json 파일 생성
tsc --init

//필요한 라이브러리 설치
npm i ts-dotenv @types/bcrypt @types/jsonwebtoken ts-mongoose 
npm i swagger-cli @types/swagger-ui-express swagger-autogen

 

2. db 연결 파일(schema/index.ts) 만들기

import "dotenv/config";
import mongoose from "mongoose";
mongoose.set('strictQuery', true);

const DB_HOST: string = process.env.DB_HOST as string;

const connect = () => {
    mongoose
        .connect(DB_HOST)
        .then(() => console.log("mongoDB Connected"))
        .catch(err => console.log(err));
};

mongoose.connection.on("error", err => {
    console.error("mongoDB connecting error", err);
});

module.exports = connect;

 

3. 서버 파일(app.ts) 만들기(+db-여기선 mongoDB- 연결)

🌟그냥 JS에서 하던대로 const로 express같은거 선언하는 것 아니고 import로 불러와야한다~!

import "dotenv/config";
import helmet from "helmet";
import express, {Express} from "express";
const app: Express = express();
import http from "http";
const server = http.createServer(app)
import routes from "./routes";

import mongoose from "mongoose";
const connect = require("./schema");
connect();

app.use(express.json());
app.use("/", routes);

const port = process.env.PORT || 3000;
server.listen(port, () => {
    console.log(`${port}번 포트로 열렸습니다.`);
});

 

4. 라우팅 분리

import { Router } from "express";
const router = Router();

import UsersController from "../controllers/users";
const usersController = new UsersController;

import { auth_middleware } from  "../middleware/auth-middleware";

//회원가입
router.post("/signup", usersController.signUp);

//유저 id 중복 검사
router.post("/Dup/Id", usersController.findDupId)

//유저 nickName 중복 검사
router.post("/Dup/Nick", usersController.findDupNick);

// 로그인
router.post("/login", usersController.login);

export default router

 

5. routes/users.router.ts

import { Router } from "express";
const router = Router();

import UsersController from "../controllers/users";
const usersController = new UsersController;
import { auth_middleware } from  "../middleware/auth-middleware";

//회원가입
router.post("/signup", usersController.signUp);


// 로그인
router.post("/login", usersController.login);

export default router;

 

6. controllers/users.ts

import "dotenv/config";
import { NextFunction, Request, Response } from "express";
import usersService from "../services/users";
import jwt from "jsonwebtoken";

class usersController {
    public usersService = new usersService();

    //회원가입
    public signUp = async (req: Request, res: Response, next: NextFunction) => {
        try {
            const {userId, nickName, email, password, confirm}
                : {userId: string, nickName: string, email: string, password: string, confirm: string}
                = req.body;

            await this.usersService.signUp({userId, nickName, email, password, confirm});

            res.status(201).json({ok: true, statusCode: 201, message: "회원가입 성공!!"});
        } catch (err: any) {
            res.status(err.status  || 400).json({ok: 0, statusCode: err.status, err: err.message})
        };
    };

    //유저 id 중복 검사
   public findDupId = async (req: Request, res: Response, next: NextFunction) => {
       const {userId}: {userId: string} = req.body;
       try {
           const findDupIdData = await this.usersService.findDupId(userId);
           res.status(201).json({findDupIdData});
       } catch (err: any) {
           res.status(400).json({message: err.message, statusCode: err.status});
       };
   };
    
    //유저 nickname 중복 검사
    public findDupNick = async (req: Request, res: Response, next: NextFunction) => {
        const {nickName}: {nickName: string} = req.body;
        try {
            const findDupNickData = await this.usersService.findDupNick(nickName);
            res.status(201).json({findDupNickData});
        } catch (err: any) {
            res.status(400).json({message: err.message, statusCode: err.status});
        };
    };

    //로그인
    public login = async (req: Request, res: Response, next: NextFunction) => {
        try {
            const {userId, password}: {userId: string, password: string}= req.body;
            
            //유효성 검사
            const login = await this.usersService.login(userId, password);
            
            if (login === null) {
                return res.status(404).send({ok: 0, statusCode: 404, errorMessage: "가입 정보를 찾을 수 없습니다."});
            };
            
            const getNickName = await this.usersService.getNickName(userId);
            const accessToken  = await this.usersService.getAccessToken(userId);
            const refreshToken = await this.usersService.getRefreshToken();

            //refreshToken DB에 업뎃
            await this.usersService.updateRefreshToken(userId, refreshToken);

            res.status(201).json({
                accessToken: `Bearer ${accessToken}}`,
                refresh_token: `Bearer ${refreshToken}`,
                nickName: {getNickName}&&nickName  //🔥
            });
        } catch (err: any) {
            res.status(err.status || 400).json({
                ok: 0,  
                statusCode: err.status, 
                message: err.message || "로그인 실패.."});
        };
    };
    
};

export default usersController;

 

7. services/users.ts

import "dotenv/config";
import { Router } from 'express';
const router = Router();
import { IUserInputDTO, userUniqueSearchInput } from "../interfaces/IUser";
import { Error } from "../interfaces/Error";
import UsersRepository from "../repositories/users";
// import PostsRepository from "../repositories/posts";
// import CommentsRepository from "../repositories/comments";
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";

const CHECK_ID = /^[a-zA-Z0-9]{4, 20}$/; // 4 ~ 15으로 변경
const CHECK_PASSWORD = /^[a-zA-Z0-9]{4, 30}$/; // 8~15 으로 변경

const  DB_HOST: string = process.env.DB_HOST as string;
const DB_SECRET_KEY: string = process.env.DB_SECRET_KEY as string;

class UserService {
    usersRepository = new UsersRepository();
    // postsRepository = new PostsRepository();
    // commentsRepository = new CommentsRepository();
    
    //회원가입
    signUp = async (
        {userId, nickName, email, password, confirm}
                        : {userId: string, nickName: string, email: string, password: string, confirm?: string}
    ) => {
        const isSameId = await this.usersRepository.findUserAccountId(userId);
        const isSameNickname = await this.usersRepository.findUserAccountNick(nickName);
        
        //유저 id 중복 검사
        if (isSameId) {
            const err: Error = new Error(`UserService Error`);
            err.status = 409;
            err.message = "이미 가입된 아이디가 존재합니다.";
            throw err;
        };
        
        //유저 nickname 중복 검사
        if (isSameNickname) {
            const err: Error = new Error(`UserService Error`);
            err.status = 409;
            err.message = "이미 가입된 닉네임이 존재합니다.";
            throw err;
        };

        //아이디가 최소 9자리가 아닐 경우
        if (!CHECK_ID.test(userId)) {
            const err: Error = new Error(`UserService Error`);
            err.status = 403;
            err.message = "아이디는 최소 4자리 이상으로 해주세요.";
            throw err;
        };

        //비밀번호 최소치가 맞지 않을 경우
        if (!CHECK_PASSWORD.test(password)) {
            const err: Error = new Error(`UserService Error`);
            err.status = 403;
            err.message = "비밀번호는 최소 4자리 이상으로 해주세요.";
            throw err;
        };

        //비밀번호와 비밀번호 확인이 맞지 않을 경우
        if (password !== confirm) {
            const err: Error = new Error(`UserService Error`);
            err.status = 403;
            err.message = "비밀번호와 확인 비밀번호가 일치하지 않습니다.";
            throw err;
        };

        const salt = await bcrypt.genSalt(11);
        //반복 횟수를 늘려보자
        password = await bcrypt.hash(password, salt);

        //자 이제 진짜 가입
        const createAccountData = await this.usersRepository.signUp({
            userId, nickName, email, password
        });
        return createAccountData;
    };

    //유저 id 중복 검사
    findDupId = async(userId: string) => {
        const findDupId = await this.usersRepository.findUserAccountId(userId);

        if (findDupId) {
            const err:Error = new Error(`UserService Error`);
            err.status = 409;
            err.message = "이미 가입된 아이디가 존재합니다.";
            throw err;
        } else{
            return "사용 가능한 아이디입니다."
        };
    };

    //유저 nickname 중복 검사
    findDupNick = async(nickName: string) => {
        const findDupNick = await this.usersRepository.findUserAccountNick(nickName);

        if (findDupNick) {
            const err:Error = new Error(`UserService Error`);
            err.status = 409;
            err.message = "이미 가입된 닉네임이 존재합니다.";
            throw err;
        } else{
            return "사용 가능한 닉네임입니다."
        };
    };

    //로그인
    login = async (userId: string, password: string) => {
        //회원 여부 체크
        const loginData = await this.usersRepository.login(userId);
        if (!loginData) {
            const err: Error = new Error(`UserService Error`)
            err.status = 403;
            err.message = "아이디를 확인해주세요.";
        throw err;
    };
        const checkPW = await bcrypt.compare(password, {loginData}&&password);  //🔥
        if (!checkPW) {
            const err: Error = new Error(`UserService Error`)
            err.status = 403;
            err.message = "패스워드를 확인해주세요.";
            throw err;
        };
        //회원 맞으면 로그인 정보 반환
        return { loginData };
    };
    
    //nickName 불러오기 by userId
    getNickName = async (userId: string) => {
        const getNickNameData = await this.usersRepository.findUserAccount(userId);
        return getNickNameData;
    };

    //accessToken 생성
    getAccessToken = async (userId: string) => {
        const accessToken = jwt.sign({userId}, DB_HOST, {expiresIn: "5m"});
    return accessToken;
    };

    //refreshToken 생성
    getRefreshToken = async () => {
        const refreshToken = jwt.sign({}, DB_SECRET_KEY, {expiresIn: "1d"});
        return refreshToken;
    };
    
    //refreshToken DB에 업뎃
    updateRefreshToken = async (userId: string, refreshToken: string) => {
        console.log(refreshToken);
        await this.usersRepository.updateRefreshToken(userId, refreshToken);
        
        const findUserAccountData = await this.usersRepository.findUserAccount(userId);
        return findUserAccountData;
    };
    
}

export default UserService;

 

8. repositories/users.ts

import Users from "../schema/users"
//import Posts from "../schema/posts"
//import Comments from "../schema/comments"
//import Bookmark from "../schema/bookmark"
import { IUserInputDTO, userUniqueSearchInput } from "../interfaces/IUser";
import moment from "moment"
const date = moment().format('YYYY-MM-DD HH:mm:ss');

class usersRepository {
    //회원가입
    signUp = async (
        {userId, nickName, email, password}
            : {userId: string, nickName: string, email: string, password: string}
    ) => {
        // create로 회원가입
        const createAccountData = await Users.create({
            userId, nickName, email, password, visible:"V", tutorial:false, createdAt: date,updatedAt: date
        });
        //await bookmark.create({ nickName });
        return createAccountData;
    };

    //유저 id 찾기
    findUserAccountId = async(userId: string) => {
        const findUserAccountData = await Users.findOne({userId});
        return findUserAccountData;
    } ;

    //유저 nickname 찾기
    findUserAccountNick = async(nickName: string) => {
        const findUserAccountData = await Users.findOne({nickName});
        return findUserAccountData;
    };
    
    //로그인
    login = async(userId: string)=> {
        const loginData = await Users.findOne({ userId: userId});
        return loginData;
    };
    
    //refreshToken 업뎃
    updateRefreshToken = async(userId: string, refreshToken: string) => {
        const  updateRefreshTokenData = await Users.updateOne(
            {userId}, {$set: {refresh_token: refreshToken}}
        );
        return updateRefreshTokenData;
    };
    
    //유저 정보 조회
    findUserAccount = async(userId: string) => {
    const findUserAccountData = await Users.findOne({userId});
    return findUserAccountData;
    };

};

export default usersRepository;