반응형

 mongoose 내장 함수인 findByID, findByIdAndUpdate, findByIdAndDelete에 대해 알아 보자.

 

(Collection은 가칭)

Collection.findByID(id) : Collection에서 _id가 id인 document를 찾는다.

Collection.findByIdAndUpdate(id, data) : Collection에서 _id가 id인 document를 찾아서 data 내용에 따라 수정한다.

Collection.findByIdAndDelete(id) : Collection에서 _id가 id인 document를 찾아서 삭제한다.

 

코드를 통해 살펴보자.

const findUserById = async (userId: string): Promise<UserResponseDto | null> => {
    try {
        const user = await User.findById(userId);
        if (!user) {
            return null;
        }

        return user;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

위 코드는 어떤 프로젝트의 Service 코드이다.

findById(나머지 둘도 마찬가지)는 document를 불러와 저장할 수 있다.

 

이때 유의할 점은, user가 존재하지 않을 수 있기 때문에(존재하지 않는 userId)

null을 반환할 수 있도록 반환 타입 명시와 반환값 설정을 위와 같이 해줘야 한다.

 

const updateMovie = async (movieId: string, movieUpdateDto: MovieUpdateDto): Promise<MovieResponseDto | null> => {
    try {
        const movie = await Movie.findByIdAndUpdate(movieId, movieUpdateDto);
        if (!movie) {
            return null;
        }

        return movie;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

이번에는 findByIdAndUpdate를 사용했다.

유의할 점은, find And Update 순서에서 눈치챌 수 있듯이,

find가 먼저 적용되기 때문에,

movie에는 update 전 document 정보가 저장된다.

 

따라서, update 후 document 정보를 저장하고 싶다면 아래와 같이 코드를 수정해야 한다.

const updateMovie = async (movieId: string, movieUpdateDto: MovieUpdateDto): Promise<MovieResponseDto | null> => {
    try {
        const movie = await Movie.findByIdAndUpdate(movieId, movieUpdateDto);
        if (!movie) {
            return null;
        }
        const data = await Movie.findById(movieId);
        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

(더 좋은 방법이 있을텐데.. 아시는 분 있으시다면 댓글로 조언 부탁드립니다. 감사합니다.)

 

 

마지막으로 findByIdAndDelete를 사용해보자.

const deleteMovie = async (movieId: string): Promise<MovieResponseDto | null> => {
    try {
        const movie = await Movie.findByIdAndDelete(movieId);
        if (!movie) {
            return null;
        }

        return movie;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

이때도 movie에는 삭제 전 정보가 저장된다.

내가 삭제한 정보에 대해서 확인하고 document를 보내주기 위해(?) 위와 같은 로직을 작성하였다.

 

 

 

이렇게 mongoose 내장 함수인 findById, findByIdAndUpdate, findByIdAndDelete를 통해

update, delete, get 메서드로 정보를 수정, 삭제, 조회하는 코드를 구현하는 방법을 공부해보았다.

 

 

 

반응형
반응형

NoSQL인 MongoDB는 Join의 개념이 없다!

하지만 분명 값을 참조해올 필요는 존재할 것이다.

 

본 포스팅에서는,

'User', 'Movie' 컬렉션과 'Review' 컬렉션이 존재할 때,

 

'Review' 컬렉션에 'movie'라는 field를 만들어 'Movie'의 값들을 전부 참조해오는 것과

'Review' 컬렉션에 'writer'라는 field를 만들어 'User'의 필드 중 'name'만 참조해오는 것을 구현해볼 것이다.

 


Collection 만들기

// Movie.ts
import mongoose from "mongoose";
import { MovieInfo } from "../interfaces/movie/MovieInfo";

const MovieSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true,
    },
    director: {
        type: String,
        required: true,
    },
    startDate: {
        type: Date,
    },
    thumbnail: {
        type: String,
    },
    story: {
        type: String,
    },
});

export default mongoose.model<MovieInfo & mongoose.Document>("Movie", MovieSchema);

Movie 컬렉션은 별다른 참조 필드 없이 작성했다.

 

import mongoose from "mongoose";
import { ReviewInfo } from "../interfaces/review/ReviewInfo";

const ReviewSchema = new mongoose.Schema({
    writer: {
        type: mongoose.Types.ObjectId,
        required: true,
        ref: "User",
    },
    movie: {
        type: mongoose.Types.ObjectId,
        required: true,
        ref: "Movie",
    },
    title: {
        type: String,
        required: true,
    },
    content: {
        type: String,
        required: true,
    }
});

export default mongoose.model<ReviewInfo & mongoose.Document>("Review", ReviewSchema);

Review 컬렉션은

writer 필드에서 User의 id를,

movie 필드에서 Movie의 id를 참조했다.

 

User는 따로 적진 않겠지만, 'name'이라는 필드를 가지고 있다.

 

컬렉션 생성에 필요한 interface 들은 아래와 같다.

// ReviewInfo.ts
import mongoose from "mongoose";

export interface ReviewInfo {
    writer: mongoose.Types.ObjectId;
    movie: mongoose.Types.ObjectId;
    title: string;
    content: string;
}

// MovieInfo.ts
export interface MovieInfo {
    title: string;
    director: string;
    startDate: Date;
    thumbnail: string;
    story: string;
}

 

 

Service에서 참조 로직 작성하기

Controller 등 다른 것들은 참조 여부와 큰 관련이 없다.

Service 로직에서 참조를 어떻게 구현하는지 살펴보자.

 

첫번째로 'updateReview'를 만들 것이다.

도입부에서 언급한대로, writer(User의 id 참조)를 통해 User의 name을 참조해오고,

movie(Movie의 id 참조)를 통해 Movie의 전체 필드를 가져올 것이다.

const updateReview = async (reviewId: string, reviewUpdateDto: ReviewUpdateDto): Promise<ReviewResponseDto | null> => {
    
    try {
        const review = await Review.findByIdAndUpdate(reviewId, reviewUpdateDto)
        .populate('writer', 'name').populate('movie');
        if (!review) {
            return null;
        }
        const data: any = {
            writer: review.writer,
            movie: review.movie,
            title: review.title,
            content: review.content
        }
        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

.populate() 메서드를 통해 참조를 구현할 수 있다.

위에서 볼 수 있듯이,

특정 필드(name)만 참조해오려면 -> .populate('필드명', '참조해올 필드명')

전체를 참조해오려면 -> .populate('필드명')

과 같이 작성한다.

 

또한 받아온 정보를 사용하기 전에 가공 과정이 필요하다.

이때, TypeScript를 사용하고 있다면,

data에 any 타입을 지정해주는 것에 유의해야 한다.

 

참고로 반환 타입인 ReviewResponseDto는 아래 코드와 같다.

import { MovieInfo } from "../movie/MovieInfo";

export interface ReviewResponseDto {
    writer: string;
    title: string;
    content: string;
    movie: MovieInfo;
}

movie에 Moive 전체 필드를 가져올 것이므로, 타입을 MovieInfo로 지정해주었다.

 

 

두번째로는 'getReviews'를 만들어 보자.

'movie'가 특정 id값과 일치하는 모든 Review documents를 불러오는 로직이다.

 

이 또한, writer(User의 id 참조)를 통해 User의 name을 참조해오고,

movie(Movie의 id 참조)를 통해 Movie의 전체 필드를 가져올 것이다.

const getReviews = async (movieId: string): Promise<ReviewResponseDto[] | null> => {
    try {
         const reviews = await Review.find({
             movie: movieId
         }).populate('writer', 'name').populate('movie');
         if (reviews.length === 0) {
            return null;
        }

         const data = await Promise.all(reviews.map((review: any) => {           
             const result = {
                writer: review.writer.name,
                movie: review.movie,
                title: review.title,
                content: review.content
             };

             return result;
         }));

        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

참조해온 방식은 위의 ' updateReview'와 동일하다.

 

여기서는 mongoose 내장 함수인 find를 활용해

Movie 컬렉션에서 movie가 받아온 movieId와 같은 모든 document의 정보를 가져오고

각각을 populate로 참조해서 적용한다.

 

한 가지 유의할 다른 점은, 참조해온 데이터 가공 방식이다.

Promise.all()과 map을 활용하는데,

map이 여러개의 Promise를 반환하기 때문에 await Promise.all()을 활용해

await 병렬 구조로 실행한다.

 

Promise.all()을 쓰지 않아도 동일한 결과를 나타내지만(적어도 저 코드에서는 그렇더라는..),

성능을 위해 Promise.all()을 사용하는 것 같다.

 

 

아래는 위에서 작성한 로직(getReviews)의 Response 예시이다.

{
    "status": 200,
    "success": true,
    "message": "리뷰 조회 성공",
    "data": [
        {
            "writer": "test1",
            "movie": {
                "_id": "627b4f23f22c32b2fcddc951",
                "title": "닥터 스트레인지",
                "director": "이우진",
                "startDate": "2022-05-01T15:00:00.000Z",
                "thumbnail": "https://example-sopt.s3.ap-northeast-2.amazonaws.com/%E1%84%80%E1%85%A7%E1%86%BC%E1%84%87%E1%85%A9%E1%86%A8%E1%84%80%E1%85%AE%E1%86%BC.jpeg",
                "story": "재밌는 영화입니다."
            },
            "title": "첫번째 리뷰",
            "content": "재밌는 영화였습니다."
        },
        {
            "writer": "test1",
            "movie": {
                "_id": "627b4f23f22c32b2fcddc951",
                "title": "닥터 스트레인지",
                "director": "이우진",
                "startDate": "2022-05-01T15:00:00.000Z",
                "thumbnail": "https://example-sopt.s3.ap-northeast-2.amazonaws.com/%E1%84%80%E1%85%A7%E1%86%BC%E1%84%87%E1%85%A9%E1%86%A8%E1%84%80%E1%85%AE%E1%86%BC.jpeg",
                "story": "재밌는 영화입니다."
            },
            "title": "두번째 리뷰",
            "content": "재미 하나도 없는 영화였습니다."
        }
    ]
}

'movie'가 '627b4f23f22c32b2fcddc951' 인 모든 Review documents를 불러오고,

'movie'는 Movie document의 모든 필드를,

'writer'는 User document의 'name' 필드를 가져온다.

 

실제 Review에는 아래와 같이 저장되어 있다.

(tmi : __v 는 버전을 나타내는 versionKey 필드인데, mongoose를 통해 데이터를 집어넣으면 생긴다고 한다. 궁금해서 방금 쳐봤다.)

 

 

이렇게 MongoDB에서 데이터를 참조하는 것을 구현해보았다.

 

 

 

 

 

 

 

반응형
반응형

express-validator는 유효성 검사를 도와주는 npm 라이브러리이다.

 

본 포스팅을 통해 아래와 같은 유효성 검사 로직을 적용해보자.

비어있지 않음 - notEmpty()

email 형식 - isEmail()

date 형식 - isDate()

error message 변경 - withMessage()

 

또한 본 포스팅에서는

router에서 validation 로직을,

controller에서 error handling 로직을 작성할 것이다.

 

 

 


 

express-validator 설치

yarn add express-validator

// 또는
npm install express-validator

 

 

router 수정 (validation 부분)

// 적용 전
import { Router } from "express";
import { UserController } from "../controllers";

const router: Router = Router();
router.post('/', UserController.createUser);

// 적용 후
import { Router } from "express";
import { body } from "express-validator/check";
import { UserController } from "../controllers";

const router: Router = Router();
router.post('/', [
    body("name")
        .notEmpty().withMessage("name이 비어있습니다."),
    body("phone")
        .notEmpty().withMessage("phone이 비어있습니다."),
    body("email")
        .notEmpty().withMessage("email이 비어있습니다.")
        .isEmail().withMessage("email 형식이 올바르지 않습니다."),
], UserController.createUser);

notEmpty() : 비어있으면 error 발생

 

메서드명이 대부분 직관적으로 이해된다.

withMessage는 바로 앞 메서드에 대해 error message를 변경해준다.

(default value는 'Invalid value'로 심플해서 웬만하면 수정하는 것이 좋다.)

 

 

// 적용 전
import express, { Router } from "express";
import { body } from "express-validator/check";
import MovieController from "../controllers/MovieController";

const router: express.Router = Router();
router.post('/', MovieController.createMovie);

export default router;



// 적용 후
import express, { Router } from "express";
import { body } from "express-validator/check";
import MovieController from "../controllers/MovieController";

const router: express.Router = Router();
router.post('/', [
    body("title")
        .notEmpty().withMessage('title이 비어있습니다.'),
    body("director")
        .notEmpty().withMessage('director가 비어있습니다.'),
    body("startDate")
        .isDate().withMessage('startDate의 형식이 올바르지 않습니다.'),
] ,MovieController.createMovie);

export default router;

isDate() : date 형식이 아니면 error 발생

 

 

이번에는 controller에서 error를 handling 해보자.

 

 

controller 수정 (error handling 로직)

// 적용 전
const createMovie = async (req: Request, res: Response): Promise<void | Response> => {
    const movieCreateDto: MovieCreateDto = req.body;
    try {
        const data = await MovieService.createMovie(movieCreateDto);
        res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, message.CREATE_MOVIE_SUCCESS, data));
    } catch (error) {
        console.log(error);
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

// 적용 후
// ...
import { validationResult } from "express-validator";
// ...

const createMovie = async (req: Request, res: Response): Promise<void | Response> => {
    const error = validationResult(req);	// validation 결과 받아오기
    if (!error.isEmpty()) {
        console.log(error);
        return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, error.array()[0].msg));
    }

    const movieCreateDto: MovieCreateDto = req.body;
    try {
        const data = await MovieService.createMovie(movieCreateDto);
        res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, message.CREATE_MOVIE_SUCCESS, data));
    } catch (error) {
        console.log(error);
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

위와 같은 로직으로 error를 받아와 그 여부를 확인(!error.isEmpty())하고  (-> error가 있으면 isEmpty() 결과가 false가 나온다.)

'error.array()[0].msg' 로 그 msg를 받아온다.

 

참고로 error에 대한 여러 정보가 error.array()에 담겨 있는데, 아래와 같다.

 

// console.log(error) 결과
Result {
  formatter: [Function: formatter],
  errors: [
    {
      value: undefined,
      msg: 'director가 비어있습니다.',
      param: 'director',
      location: 'body'
    }
  ]
}

// console.log(error.array()) 결과
[
  {
    value: undefined,
    msg: 'director가 비어있습니다.',
    param: 'director',
    location: 'body'
  }
]

저 배열 값을 활용하려고 이것 저것 써봤는데..

답은 error.array() 였다!

 

 

그 외에도 많은 기능과 용법을 공식 문서에서 확인할 수 있다!

https://express-validator.github.io/docs/

 

Getting Started · express-validator

express-validator is a set of [express.js](http://expressjs.com/) middlewares that wraps

express-validator.github.io

 

 

읽어주셔서 감사합니다.

 

 

 

반응형
반응형

node와 express를 활용한 프로젝트를 시작하고

간단한 CRUD api를 작성해보는 포스팅을 적어보려 한다.

 

프로젝트를 시작할 때 참고하면 좋을 것 같다.

 

DB 생성과 초기 세팅은 이전 포스팅 참고.

 

MongoDB Cluster 생성하기 (링크🔗)

Express 초기 세팅, yarn 사용법 (링크🔗)

 

 

 


 

.env 만들기

루트 디렉토리에 '.env' 파일을 만든다 (* 반드시 .gitignore에 추가한다.)

 

 

// .env
PORT=8080
MONGODB_URI=mongodb+srv://id:password@clusterName.dgufc.mongodb.net/어쩌구저쩌구?retryWrites=true&w=majority

위와 같이 입력하는데, 'MONGOB_URI'는 MongoDB 홈페이지에서 가져온다

 

홈페이지에서 로그인하면 아래 화면이 바로 나온다.

 

 

 

 

기타 파일 생성 및 수정

1. 'config/index.ts' 생성

import dotenv from "dotenv";

// Set the NODE_ENV to 'development' by default
process.env.NODE_ENV = process.env.NODE_ENV || "development";

const envFound = dotenv.config();
if (envFound.error) {
  // This error should crash whole process

  throw new Error("⚠️  Couldn't find .env file  ⚠️");
}

export default {
  /**
   * Your favorite port
   */
  port: parseInt(process.env.PORT as string, 10) as number,

  /**
   * MongoDB URI
   */
  mongoURI: process.env.MONGODB_URI as string,
};

 

2. 'loaders/db.ts' 생성

import mongoose from "mongoose";
import config from "../config";

const connectDB = async () => {
    try {
        await mongoose.connect(config.mongoURI);

        mongoose.set('autoCreate', true);

        console.log("Mongoose Connected ...");
    } catch (err: any) {
        console.error(err.message);
        process.exit(1);
    }
};

export default connectDB;

 

3. 'src/index.ts' 수정

import express, { Request, Response, NextFunction } from "express";
import config from "./config";
const app = express();
import connectDB from "./loaders/db";
import routes from './routes';
require('dotenv').config();

connectDB(); // DB 연결하기

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use(routes);   //라우터 분리
// error handler

interface ErrorType {
  message: string;
  status: number;
}

// 모든 에러에 대한 핸들링
app.use(function (err: ErrorType, req: Request, res: Response, next: NextFunction) {

  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "production" ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render("error");
});

app
  .listen(config.port, () => {
    console.log(`
    ################################################
          🛡️  Server listening on port 🛡️
    ################################################
  `);
  })
  .on("error", (err) => {
    console.error(err);
    process.exit(1);
  });

 

 

4. modules 폴더 만들기

// modules/util.ts -> success, fail 메세지 가공
const util = {
    success: (status: number, message: string, data?: any) => {
        return {
            status,
            success: true,
            message,
            data,
        };
    },
    fail: (status: number, message: string, data?: any) => {
        return {
            status,
            success: false,
            message,
        };
    },
};

export default util;

// modules/responseMessage.ts -> response message 가공
const message = {
    NULL_VALUE: '필요한 값이 없습니다.',
    NOT_FOUND: '존재하지 않는 자원',
    BAD_REQUEST: '잘못된 요청',
    INTERNAL_SERVER_ERROR: '서버 내부 오류',

    // 포스팅 조회
    READ_POST_SUCCESS: '포스팅 조회 성공',
    CREATE_POST_SUCCESS: '포스팅 생성 성공',
    DELETE_POST_SUCCESS: '포스팅 삭제 성공',
    UPDATE_POST_SUCCESS: '포스팅 수정 성공',
}

export default message;

// modules/statusCode.ts
const statusCode = {
    OK: 200,
    CREATED: 201,
    NO_CONTENT: 204,
    BAD_REQUEST: 400,
    UNAUTHORIZED: 401,
    FORBIDDEN: 403,
    NOT_FOUND: 404,
    CONFLICT: 409,
    INTERNAL_SERVER_ERROR: 500,
    SERVICE_UNAVAILABLE: 503,
    DB_ERROR: 600,
};

export default statusCode;

 

 

라우팅 설정하기

routes/PostRouter.ts

import { Router } from "express";
import { PostController } from "../controllers";

const router: Router = Router();

router.post('/', PostController.createPost);
router.put('/:postId', PostController.updatePost);
router.get('/:postId', PostController.findPostById);
router.delete('/:postId', PostController.deletePost);

export default router;
위에서 작성한대로 나머지를 작성하려고 한다.
PostController.ts를 만들어 createPost, updatePost, findPostById, deletePost 4개의 함수를 만든다.
put, get, delete 메서드에 대해서는 postId를 파라미터로 받는다.

 

routes/index.ts

//router index file
import { Router } from 'express';
import PostRouter from "./PostRouter";

const router: Router = Router();

router.use('/post', PostRouter);

export default router;

 

 

DTO 작성

DTO는 Data Type Object의 약자로,

데이터를 받아오거나 반환하는 규격으로 interface로 작성한다.

(장고의 serializer 생각하면 편하더라는..)

 

// interfaces/post/PostCreateDto.ts -> create 용
export interface PostCreateDto {
    title: string;
    content: string;
    additional?: {
        category: string;
        season: string;
    };
}

// interfaces/post/PostInfo.ts -> model 정의 용도
export interface PostInfo {
    title: string;
    content: string;
    additional: {
        category: string;
        season: string;
    };
}

// interfaces/post/PostResponseDto.ts -> 정보 조회 용
import mongoose from "mongoose";
import { PostCreateDto } from "./PostCreateDto";

export interface PostResponseDto extends PostCreateDto { // PostCreateDto 를 상속받아 확장시킨다.
    _id: mongoose.Schema.Types.ObjectId;
}


// interfaces/post/PostUpdateDto.ts -> update 용
import mongoose from "mongoose";

export interface PostUpdateDto {
    _id: mongoose.Schema.Types.ObjectId;
    title?: string;
    content?: string;
    additional?: {
        category: string;
        season: string;
    };
}

// interfaces/common/PostBaseResponseDto.ts -> id만 노출할 때
import mongoose from "mongoose";

export interface PostBaseResponseDto {
    _id: mongoose.Schema.Types.ObjectId;
}

다른 로직 작성하다가 필요에 맞게 DTO를 작성해주는 것이 좋다.

 

 

model 작성

models/Post.ts 를 만든다.

import mongoose from "mongoose";
import { PostInfo } from "../interfaces/post/PostInfo";

const PostSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true
    },
    content: {
        type: String,
        required: true,
    },
    dateTimeOfPosting: {
        type: Date,
        required: true,
        default: Date.now,
    },
    additional: {
        category: { type: String },
        season: { type: String },
    },
});

export default mongoose.model<PostInfo & mongoose.Document>("Post", PostSchema);

dateTimeOfPosting은 생성 당시 일시를 저장한다.

additional은 중복 document(MongoDB에서 row에 해당하는 개념)



 

Controller 작성

import express, { Request, Response } from "express";
import { PostCreateDto } from "../interfaces/post/PostCreateDto";
import { PostUpdateDto } from "../interfaces/post/PostUpdateDto";
import statusCode from "../modules/statusCode";
import message from "../modules/responseMessage";
import util from "../modules/util";
import { PostService } from "../services";


const createPost = async (req: Request, res: Response): Promise<void> => {
    const postCreateDto: PostCreateDto = req.body;
    
    try {
        const data = await PostService.createPost(postCreateDto);
        res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, message.CREATE_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

const updatePost = async (req: Request, res: Response): Promise<void> => {
    const postUpdateDto: PostUpdateDto = req.body;
    const { postId } = req.params;

    try {
        const data = await PostService.updatePost(postId, postUpdateDto);
        res.status(statusCode.CREATED).send(util.success(statusCode.OK, message.UPDATE_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

const findPostById = async (req: Request, res: Response): Promise<void> => {
    const { postId } = req.params;

    try {
        const data = await PostService.findPostById(postId);
        res.status(statusCode.CREATED).send(util.success(statusCode.OK, message.READ_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

const deletePost = async (req: Request, res: Response): Promise<void> => {
    const { postId } = req.params;

    try {
        const data = await PostService.deletePost(postId);
        res.status(statusCode.CREATED).send(util.success(statusCode.OK, message.DELETE_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

export default {
    createPost,
    updatePost,
    findPostById,
    deletePost,
}

Request body는 req.body에, Request parameters는 req.params에 저장되어 있어서

필요한 경우에 맞게 할당시켜 사용한다.

 

controllers/index.ts도 수정한다.

import PostController from "./PostController"

// controller index file
export {
   PostController,
}

 

 

Service 작성

import { PostBaseResponseDto } from "../interfaces/common/PostBaseResponseDto";
import { PostCreateDto } from "../interfaces/post/PostCreateDto";
import { PostResponseDto } from "../interfaces/post/PostResponseDtd";
import { PostUpdateDto } from "../interfaces/post/PostUpdateDto";
import Post from "../models/Post";



const createPost = async (postCreateDto: PostCreateDto): Promise<PostBaseResponseDto> => {
    try {
    	// create를 위해 각 filed명에 값들을 할당시켜준다.
        const post = new Post({
            title: postCreateDto.title,
            content: postCreateDto.content,
            additional: {
                category: postCreateDto.additional?.category,
                season: postCreateDto.additional?.season,
            }
        });
        await post.save();

        const data = {
            _id: post.id
        };

        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

const updatePost = async (postId: string, postUpdateDto: PostUpdateDto): Promise<PostUpdateDto | null> => {
    try {
        await Post.findByIdAndUpdate(postId, postUpdateDto); // update 로직
        const post = await findPostById(postId); // update 된 정보를 불러오는 로직
        // null이 될 경우를 처리해줘야 한다.
		if (!post) {
            return null;
        }
        return post;

    } catch (error) {
        console.log(error);
        throw error;
    }
}

const findPostById = async (postId: string): Promise<PostResponseDto | null> => {
    try {
        const post = await Post.findById(postId);
        if (!post) {
            return null;
        }
        return post;
        
    } catch (error) {
        console.log(error);
        throw error;
    }
}

const deletePost = async (postId: string): Promise<PostResponseDto | null> => {
    try {
        const post = await Post.findByIdAndDelete(postId);
        if (!post) {
            return null;
        }
        return post;
    } catch (error) {
        console.log(error)
        throw error;
    }
}

export default {
    createPost,
    updatePost,
    findPostById,
    deletePost,
}

findById와 같은 메서드들은 mongoose 내장 함수이다.

용법이 그렇게 어렵지 않다.

 

 services/index.ts 도 수정해준다.

import PostService from "./PostService"

//service index file
export {
    PostService,
}

 

이렇게 작성이 끝이 났다.

 

 

요청 보내기

yarn run dev 실행

 

POST

PUT

userId는 무시해도 좋다(포스팅 내용과 다른 코드 때문)

 

GET

 

DELETE

 

 


 

프로젝트 구조에 대해 간단히 언급하고 포스팅을 마치려 한다.

 

Request <--> Controller <--> Service <--> Response(DB)

 

Controller는 Request 데이터를 적절히 가공해서 Service에 전달하는 역할만 한다.

Service는 가공된 데이터를 가지고 원하는 로직을 적용한 뒤 DB에 요청을 전달해 결과값을 받아 온다.

그리고 그 과정에서 DTO라는 틀을 통해 데이터를 주고 받는다.

 

Service 단에서 사용하는 로직을 '비즈니스 로직'이라고 한다.

이 '비즈니스 로직'을 Route, Controller에서 분리시켜 재사용, 유지보수가 용이하게 한다.

 

Request 객체를 받아오고 Response 객체를 만들어 전달하는 것을 Controller에서,

그것에 원하는 로직을 적용하는 것은 Service에서 실행하는 것이다.

 


 

이렇게 express 프로젝트를 생성하고 간단하게 모델과 CRUD api를 작성해서 테스트해보았다.

앞으로 node, express, MongoDB를 사용하는 프로젝트가 있다면,

본 포스팅 코드를 기반으로 서버 구현을 시작해도 좋을 것 같다.

 

 

 

반응형
반응형

 

가입하기

 

https://www.mongodb.com/cloud/atlas/lp/try2?utm_source=google&utm_campaign=gs_apac_south_korea_search_core_brand_atlas_desktop&utm_term=mongodb&utm_medium=cpc_paid_search&utm_ad=e&utm_ad_campaign_id=12212624365&adgroup=115749706703&gclid=EAIaIQobChMI-L2Ysr6i9wIVxcCWCh3VFAbSEAAYASAAEgKvQ_D_BwE 

 

MongoDB Atlas: Cloud Document Database

Cloud-hosted MongoDB service on AWS, Azure, and GCP

www.mongodb.com

MongoDB Atlas 사이트에서 가입.

 

동의하고 'Submit' 클릭

잠시 대기하기

 

원하는 용도 선택하기

 

연습용이니 Free 선택

 

다른 건 수정하지 말고 Cluster Name만 설정

 

Quick Start로 들어오면,

DB에 접근할 수 있는 user를 새로 생성해야한다.

Username, Password를 반드시 기억한다.

 

 

Network Access 설정

좌측 메뉴에서 Network access 메뉴,

가운데 Add IP Adress 클릭

 

Access List Entry에 사용하는 서버 IP를 입력해야 하나,

연습 중이라 서버가 없으니 상단에 'ALLOW ACCESS FROM ANYWHERE'를 클릭하고 Confirm!

 

좌측 메뉴에서 Database 클릭

만들었던 Cluster 내부에 Connect 클릭

 

MongoDB Compass를 클릭하여 GUI를 사용해보자

 

1번 - OS에 맞는 프로그램 설치 후

2번 - Connecting String 복사

 

MongoDB Compass App 실행 후 'New Connection'에 아까 복사한 'Connect String' 붙여넣기

<password> 를 아까 만든 DB user의 password로 교체한다

* <> 꺾쇠까지 포함해서 실제 pw로 교체해야한다! <> 안에 넣는 것이 아니다.

 

ExampleCluster와 Connection이 성공했다면 끝!

반응형
반응형

헷갈리는 동기, 비동기 개념을 정리하는 포스팅 (3)

 

1) callback에 대한 이해🔗 : https://woojin.tistory.com/27

2) JS 초짜가 Promise 완벽하게 이해하기🔗 : https://woojin.tistory.com/28

 

 

이번 포스팅은, async / await 키워드의 사용법과 의미에 대해

無의 상태로 돌아가서 공부하고 정리해보고자 한다.

(* 오류에 대한 지적 환영합니다.)

 

첫째로는, async/await의 사용법, 의미에 대해서 자세히 다룰 것이고

둘째로는, await의 유무와 비동기 처리에 관계에 대해 이해해볼 것이다.

마지막으로는, 비동기 코드를 병렬 처리하는 .all(), .race() 메서드에 대해 알아볼 것이다.

 

(유튜브 채널 '드림코딩'의 영상 코드를 토대로 공부했다.)

https://www.youtube.com/watch?v=aoQSOZfz3vQ 

 


async란? (+ 사용법)

async와 await은 비동기를 동기적으로 작성할 수 있는 키워드이다.

이게 도대체 뭔소린지 전혀 이해가 되지 않았다.

 

우선 Promise를 이해했으니,

Promise가 async로 대치되는 코드를 살펴보자.

 

// <<< Promise >>>
const fetchUser = () => {
    return new Promise((resolve, reject) => {
        // do network request in 10 secs ...
        resolve('woojin');
    });
}

const user = fetchUser();
user.then(console.log);

 

// <<< Async >>>
const fetchUserAsync = async () => {
    // do network request in 10 secs ...
    console.log('loading...');
    return 'woojin';
}

const user = fetchUser();
user.then(console.log);

보다시피 Promise에서 resolve하던 것을,

async 키워드를 붙이면 그냥 return하면 된다!

이렇게 Promise, resolve를 통해 느껴지던 동기 코드와의 이질감을 해소한다.

(reject는? -> throw error, try catch 사용하면 되는데, 에러 핸들링에 대해서는 자세히 다루지 않겠다.)

 

 

await이란? (+ 사용법)

await은 기다려주는 키워드이다!

이렇게 간단한 키워드가 죽어라 이해가 안 되었는데, 추가 설명이 필요하기 때문이다. 바로바로..

 

"await은 Promise를 반환하는 함수에 쓰여야 한다."

이해하고 나니 너무 당연하다.

서버에 api를 요청해서 유저의 mbti 정보를 받아와서 infp가 맞는지 여부를 반환한다고 생각해보자.

서버에 유저 mbti를 요청하고 받아오는 동안 여부를 확인하는 코드는 동작하면 안 된다!

"mbti 알아올 때까지 기다려~" 라고 말해줘야 하는 상황.

 

그렇기에 Promise를 반환하고 .then 메서드를 사용한 것 아닌가?

 

체이닝은 왜 사용했는가?

서버에서 유저의 mbti를 받아온 다음, 서버에 다시 mbti를 보내고 해당 mbti의 특징을 받아온다고 해보자.

mbti를 받아온 다음 특징을 받아와야 하기 때문에

mbti().then(mbti => 특징받아오기()).then(특징 => console.log(특징));

이런 식으로 체이닝 한 것이 아닌가?

 

아래 코드를 통해 정확히 이해해보자.

const delay = (ms) => {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const getApple = async () => {
    await delay(4000);
    return '🍎';
}

const getBanana = async () => {
    await delay(2000);
    return '🍌';
}

getApple().then(console.log);
getBanana().then(console.log);

delay는 단순히 시간을 지연시키는 함수이다.

getApple은 delay를 통해 4초 멈추고 🍎를 반환하고, getBanana는 2초 멈추고 🍌를 반환한다.

 

위 코드를 실행하면 getApple(), getBanana() 순서로 Promise를 호출하기는 하지만,

비동기적으로 delay 함수가 실행되어 🍌, 🍎 순서로 출력된다.

 

그럼 마치 mbti를 알아야 그 특성을 아는 것처럼,

사과를 받아야 바나나를 받을 수 있다고 가정하고 순서대로 호출해보자.

먼저 promise chaning을 사용하면 아래와 같다.

const pickFruits = () => {
    return getApple().then(apple => {
        return getBanana().then(banana => `${apple} + ${banana}`);
    })
};

 apple에 🍎를 받아와 다시 promise를 반환하는 함수인 getBanana에 넣는다.

어라라? 어딘가 익숙하지 않은가? callback 지옥이 떠오른다..

이런 식으로 chaining한다면 callback 지옥과 다를 것이 없다.

 

하지만 async/await을 사용하면 아래와 같다.

const pickFruits = async () => {
    const apple = await getApple();
    const banana = await getBanana();
    return `${apple} + ${banana}`;
}

promise, .then()이 async와 await으로 대체된다.

본 코드는 4초 후 apple에 🍎를 저장하고, 다시 2초 후 banana에 🍌를 저장한다.

총 6초 후, '🍎 + 🍌'를 반환한다.

 

 

await 유뮤의 차이점

필자는 위 코드를 처음 봤을 때, await의 정확한 의미를 파악하지 못했다.

await이 있고 없고의 차이를 비교하면서 await의 정확한 용도를 파악할 수 있었다.

아직 await의 의미가 헷갈린다면, 아래 코드를 참고해보자.

 

const delayApple = (ms) => {
    return new Promise(resolve => setTimeout(() => resolve('🍎'), ms));
}

const getApple = async () => {
    a = await delayApple(10000); // await 사용
    fruit = a + '🍌'
    return fruit;
}

getApple().then(console.log);

* 참고로 setTimeout의 인자로 들어간 콜백 함수는 'resolve' 꼴이면 가능한데 'resolve('🍎') 꼴이면 안 된다.

resolve()는 함수라기보다 결과에 속하기 때문에, '() => resolve('🍎')' 꼴로 넣어줘야 한다.

 

위 코드에서, await 키워드 때문에 10초간 delayApple의 결과를 기다렸다가 a에 넣은 후에야

fruit = a + '🍌'가 실행된다. (해당 줄은 Promise를 반환하는 함수가 쓰인 것도 아니고 순서대로 실행하면 되므로 await을 넣을 이유가 없다!)

 

그렇다면 await이 없는 코드는 어떻게 작동할까?

const delayApple = (ms) => {
    return new Promise(resolve => setTimeout(() => resolve('🍎'), ms));
}

const getApple = async () => {
    a = delayApple(10000); // await 사용
    // console.log(a)
    fruit = a + '🍌'
    return fruit;
}

getApple().then(console.log);

해당 코드의 출력은 '[object Promise]🍌' 이다.

그리고 10초 후에 프로그램이 종료된다.

 

delayApple은 10초 후에 resolve('🍎')를 가져오는데,

promise였다면 .then()을 통해 기다렸겠지만,

위 코드에서는 기다리는 코드가 없다!

 

따라서 a에는 [object Promise]라는 빈 껍데기가 할당된 채로 fruit = a + '🍌'가 실행되고 만다.

그리고 10초 후에 a에는 정상적으로 🍎가 저장되지만, 함수는 이미 리턴값을 반환했기에 의미가 없다.

그렇게 10초가 지나서야 프로그램이 종료된다.

 

여기서 위 코드의 주석처리된 console.log(a)를 실행하면 'Promise { <pending> }'라고 출력된다!

대기(pending)상태의 Promise 객체라는 것이다.

 

 

필자는 이렇게 async, await을 이해했다.

 

간단하게 정리하자면,

async는 promise를 반환하는 함수에 붙이기만 하면 되고,

await은 promise를 반환하는 함수를 사용할 때 그것을 기다려야하면(비동기적으로 처리해야하면) 앞에 붙인다.

 

 

다음에서는 .all() - (await 병렬 처리 후 동시 반환), .race() - (병렬 처리 후 우선 처리된 promise 반환)

이렇게 2개의 메서드에 대해 알아보자.

 

 

await 병렬 처리(동시 반환, 우선 처리된 Promise 반환)

const delay = (ms) => {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const getApple = async () => {
    await delay(4000);
    return '🍎';
}

const getBanana = async () => {
    await delay(2000);
    return '🍌';
}

// .all() 메서드
const pickAllFruits = () => {
    return Promise.all([getApple(), getBanana()]).then(fruits =>
        fruits.join(' + ')
    );
}

pickAllFruits().then(console.log);

위 코드는 .all() 메서드를 활용해 두 개의 Promise 객체를 동시에 호출한 것이다.

위에서 예로 든 'mbti 받아오기', '해당 mbti 특성 받아오기'는 순차적으로 실행되어야 한다.

 

하지만, 'mbti 받아오기'와 '성별 받아오기'는 순차적으로 실행될 필요가 없다!

이런 경우 위와 같이 .all()메서드를 활용하면 반환된 Promise 객체들이 배열에 저장된다.

(당연한 얘기지만 await이 아니라 그냥 Promise 코드에도 사용할 수 있다.)

 

const pickOnlyOne = () => {
    return Promise.race([getApple(), getBanana()]);
}

pickOnlyOne().then(console.log);

.race() 메서드는 .all() 메서드와 동일하게 사용되는데, 가장 먼저 반환된 Promise 객체 한 개만 반환한다!

위 코드에서는 getApple()이 실행되는데 4초, getBanana()가 2초 걸리므로

🍌가 반환되어 출력된다.

 

 

Typescript로의 변환은 너무 간단해서 일부만 올려두겠다.

const delay = (ms: number): Promise<void> => {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const getApple = async (): Promise<string> => {
    await delay(4000);
    return '🍎';
}

const pickFruits = async (): Promise<string> => {
    const apple: string = await getApple();
    const banana: string = await getBanana();
    return `${apple} + ${banana}`;
}

위 코드로의 변환이면, 본 포스팅에 쓰인 코드를 모두 변환할 수 있다.

 

 

 


 

이렇게 3편에 걸친 동기/비동기 포스팅이 끝이 났다.

 

1도 이해 안 가던 것들이 수많은 아티클을 정독하며 결국엔 이해되어 굉장히 뿌듯하고,

나만의 방식으로 이해한 내용을 정리해두었다는 점에 든든한 마음도 든다.

 

더군다가 정이 가지 않던 JS, TS와도 굉장히 친해져서 좋았다!

 

질문이나 태클 언제든 환영입니다.

감사합니다.

 

 

 

반응형
반응형

헷갈리는 동기, 비동기 개념을 정리하는 포스팅 (2)

 

첫번째 포스팅에서는, callback의 개념과 사용 방법에 대해 다뤘다.

(콜백 지옥을 통해 화살표 함수의 사용법까지 매운맛으로 이해해보았다.)

이전 포스팅🔗 : https://woojin.tistory.com/27

 

 

이번 포스팅은, 비동기 처리를 위한 객체 Promise에 대해

無의 상태로 돌아가서 공부하고 정리해보고자 한다.

 

첫째로는, Promise의 사용법에 대해서 자세히 다룰 것이고

둘째로는, 비동기 처리에서 Promise를 사용하는 방식, 이유에 대해 고찰해보고자 한다.

 

(유튜브 채널 '드림코딩'의 영상 코드를 토대로 공부했다.)

https://youtu.be/JB_yU6Oe2eE

 


Promise란?

정의와 상태에 대해서는 다른 좋은 아티클이 많아 짚고만 넘어가자.

Promise는 비동기 처리를 위한 객체로,

resolve와 reject라는 두 개의 함수를 인자로 전달받는다.

 

Promise 객체가 호출되면 대기(Pending)상태,

Promise 내부 로직을 성공하면 이행(Fulfilled), 실패하면 실패(Rejected)상태가 된다.

 

// 1. Producer
// when new Promise is created, the executor runs automatically.

// Promise를 정의
const testPromise = new Promise((resolve, reject) => {
    // doing some heavy work (network, read files)
    console.log('doing something...');
    setTimeout(() => {
        resolve('woojin');
        // reject(new Error('no network'));
    }, 500);
});

// Promise를 반환
const testPromise = () => new Promise((resolve, reject) => {
    // doing some heavy work (network, read files)
    console.log('doing something...');
    setTimeout(() => {
        resolve('woojin');
        // reject(new Error('no network'));
    }, 500);
});

사용은 위와 같이 하는데, resolve가 성공적으로 실행되면 'woojin'을 반환한다.

실패해서 reject가 실행되면 'no network'라는 메세지를 가진 Error 객체가 반환된다.

(위의 두가지 방법이 모두 사용 가능하다.)

 

반환된 객체의 사용 방법은 아래와 같다.

// 2. Consumers: then, catch, finally
testPromise
    .then(name => console.log(name))
    .catch(error => console.log(error));
    
testPromise
    .then(console.log)
    .catch(console.log);

.then : 함수 하나를 인자로 받고, resolve 값을 가져와서 함수에 집어 넣는다.

(함수를 인자로 받는다 -> 해당 함수를 callback이라고 볼 수 있음)

.catch : 함수 하나를 인자로 받고, reject 값을 가져와서 함수에 집어 넣는다.

 

위 코드블록에서 위에 있는 testPromise

 

반대로 생각하면,

resolve가 실행되면(Promise가 성공하면) .then을 사용할 수 있고

reject가 실행되면(Promise가 실패하면) .catch를 사용할 수 있다.

 

여기서 유의할 점은,

then과 catch는 'woojin'이라는 문자열을 사용하지 않는다.

'woojin'이라는 문자열을 반환하는 Promise객체를 전달받아서 사용하는 것이고, 또다시 Promise객체를 반환한다.

 

이것은 Promise Chaining이라는 활용으로 이어진다.

아래에서 살펴보자.

(일단 여기까지는 비동기랑 Promise랑 뭔 상관인지 잘 모를 것이다. 나는 그랬다.)

 

 

Promise Chaining

then이랑 catch가 Promise 객체를 반환한다고 위에서 언급했다.

이게 뭔 소린지 사실 직관적으로 이해하기는 어려울 것이고,

아래의 활용을 잘 살펴보면 이해가 될 것이다.

 

const fetchNumber = new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000);
});

fetchNumber
    .then(num => num * 2)
    .then(num => num * 3)
    .then(num => {
        return new Promise((resolve, reject) => {
            setTimeout(() => resolve(num - 1), 1000);
        });
    })
    .then(num => console.log(num));

fetchNumber는 호출 시, 1초 후 resolve(1)을 반환하는 Promise 객체이다.

 

fetchNumber가 호출되고, 정상적으로 내부의 setTimeout이 작동했다면

.then 내부에 있는 'num => num * 2' 라는 함수에 1이 전달된다.

 

'num => num * 2' 라는 함수가 정상적으로 작동했다면, resolve(2)를 반환할 것이고

.then(num => num * 3) 을 통해서 불러올 수 있다.

그리고 이것이 정상적으로 작동했다면 resolve(6)을 반환하는데,

 

그 다음 .then을 통해서 resolve(6)을 불러온 후,

'num을 받아 Promise ~~~ 를 반환하는 함수'에 넣는다.

정상적으로 작동한다면, 6을 받아 1초 후 resolve(5)를 반환할 것이다.

 

그 후, resolve(5)가

.then(num => console.log(num)) 으로 전달되어

5가 출력된다.

 

이런 방식을 Promise Chaining이라고 한다.

지난 포스팅에서 다룬 callback만을 사용했다면 굉장히 복잡했을 코드가,

여러 함수를 연쇄적으로 호출했음에도 아주 간단해졌다.

 

 

Error Handling

const getHen = () => 
    new Promise((resolve, reject) => {
        setTimeout(() => resolve('닭'), 1000);
    });

const getEgg = hen => 
    new Promise((resolve, reject) => {
        // setTimeout(() => resolve(`${hen} => 달걀`), 1000);
        setTimeout(() => reject(new Error(`${hen} => 달걀`)), 1000);
    });

const cook = egg => 
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(`${egg} => 프라이`), 1000);
    });

getHen()//
.then(hen => getEgg(hen))
.then(egg => cook(egg))
.then(meal => console.log(meal))
.catch(error => console.log(error));

getHen()이 호출되었다.

setTimeout이 정상적으로 작동한다면, resolve('닭')이 .then을 통해 'hen => getEgg(hen)' 함수로 들어간다.

 

'닭'을 받아 getEgg 내부 로직을 실행하는데,

reject가 실행되었다! 에러가 발생한 것이다.

 

그렇다면 resolve는 실행되지 않고, 'reject(new Error(`${hen} => 달걀`)) 이 반환될 것이다.

하지만 이는 그 다음 .then(egg ...)로 전달되지 않는다.

.then은 resolve를 받아오는 함수이기 때문이다.

 

순서와 관계없이, 마지막에 있는 .catch로 전달되어 `${hen} => 달걀`에 해당하는 '닭 => 달걀'이 출력된다.

 

getHen()//
.then(hen => getEgg(hen))
.catch(error => {
    return '밥';
})
.then(egg => cook(egg))
.then(meal => console.log(meal))
.catch(error => console.log(error));

위처럼 에러가 발생한 지점에서 .catch를 실행할 수 있다.

getEgg에서 에러가 발생했으니 .catch가 실행되는데,

.catch 내부에는 '밥'을 반환하는 함수가 들어 있다.

 

결국 앞에서 실행된 로직과 관계 없이 '밥'이 반환된다.

여기서 유의할 점은!

.then과 .catch는 Promise 객체를 반환한다고 했다.

.catch 내부에서 '밥'을 반환하는 함수가 정상 작동되었으니,

이는 resolve('밥')의 형태로 전달되고, 바로 아래에 있는 .then(egg => cook(egg))에 전달된다!

 

 

callback지옥과 비교

// Callback Hell example

class UserStorage {
    loginUser(id, password, onSuccess, onError) {
        setTimeout(() => {
            if (
                (id === 'woojin' && password === 'hustle') ||
                (id === 'money' && password === 'many')
            ) {
                onSuccess(id);
            } else {
                onError(new Error('not found'));
                console.log(typeof Error);
            }
        }, 2000);
    }

    getRoles(user, onSuccess, onError) {
        setTimeout(() => {
            if (user === 'woojin') {
                onSuccess({ name: 'woojin', role: 'admin' });
            } else {
                onError(new Error('no access'));
            }
        }, 1000);
    }
}

const userStorage = new UserStorage();
const id = 'woojin';
const password = 'hustle1';
userStorage.loginUser(
    id,
    password,
    user => {
        userStorage.getRoles(
            user,
            userWithRole => {
                console.log(`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`);
            },
            error => {
                console.log(error);
            }
        );
    },
    error => {
        console.log(error);
    }
);

지난 포스팅에서 작성한 callback hell 코드이다.

Promise를 활용하면 아래와 같이 간결하게 작성할 수 있다.

 

// Callback Hell example

class UserStorage {
    loginUser(id, password) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (
                    (id === 'woojin' && password === 'hustle') ||
                    (id === 'money' && password === 'many')
                ) {
                    resolve(id);
                } else {
                    reject(new Error('not found'));
                }
            }, 2000);
        });
    }

    getRoles(user) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (user === 'woojin') {
                    resolve({ name: 'woojin', role: 'admin' });
                } else {
                    reject(new Error('no access'));
                }
            }, 1000);
        });
    }
}

const userStorage = new UserStorage();
const id = 'money';
const password = 'many';

userStorage.loginUser(id, password)
    .then(id => userStorage.getRoles(id))
    .then(value => console.log(value))
    .catch(error => console.log(error));


// userStorage.loginUser(
//     id,
//     password,
//     user => {
//         userStorage.getRoles(
//             user,
//             userWithRole => {
//                 console.log(`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`);
//             },
//             error => {
//                 console.log(error);
//             }
//         );
//     },
//     error => {
//         console.log(error);
//     }
// );

여기서 집중할 부분은 하단에 Promise 객체를 호출하는 부분이다.

그 아래에 있는 주석은 callback hell에서 사용했던 코드인데,

Promise chaining으로 간결하게 작성되었다!

 

즉, Promise를 통해 callback hell에서 벗어날 수 있었다.

 

그런데, 이게 비동기랑 도대체 무슨 상관일까?

 

 

비동기와 Promise

Promise의 사용에 대한 수많은 아티클을 보며 사용하는 방법에 대해서 감을 잡을 수 있었다.

그러나 Promise와 비동기 사이에 도대체 무슨 관계가 있는지 정확하게 이해가 되지 않았다.

그래서, 나름대로의 고민을 통해 내린 결론을 적어보려고 한다.

 

우선 비동기처리와 callback의 관계부터 살펴보자.

 

비동기처리라 함은,

A();

B();

와 같은 순서로 코드가 작성되었을 때,

 

A와 B의 처리 시간에 따라

A보다 B의 결과가 먼저 반환될 수도 있는 처리 방식을 의미한다.

 

그런데, B가 A보다 먼저 처리된다는 게 말이 되나?

그렇다 말이 안된다.

코드 순서상 A가 호출된 다음 B가 호출되는 게 맞으니까 당연히 A가 처리되고 B가 처리된다. (JS는 싱글 스레드 언어이므로 한 번에 한 작업을 실행한다.)

 

그렇다면, 비동기 처리라는 것은 B가 먼저 처리되는 것이 아니라,

B의 내부 로직이 A의 내부 로직보다 먼저 처리되는 것이라고 이해해야 한다.

이는 CPU 자원을 분할해서, 메인 스레드에서는 A를 호출하고 B를 호출하면서,

백그라운드에서 A의 내부로직과 B의 내부로직을 처리한다고 이해하면 될 것 같다.

 

따라서,

A(C) {
	C;
}

B(D) {
	D;
}

(A, B, C, D는 모두 함수)

이처럼, A를 호출하면 백그라운드에서 C가 실행되고,

B를 호출하면 백그라운드에서 D가 실행되고,

 

C, D의 처리 시간에 따라 코드 순서와 관계 없이 값을 반환하거나 함수를 처리하는 것이다.

이러한 비동기 처리를 위해 함수의 인자로 함수를 집어넣어 호출하는 callback이 필요했던 것이다.

 

 

그런데, callback은 매우 복잡한 코드를 낳는다.

지난 포스팅에서 쉽게 살펴볼 수 있었다(위에도 코드가 첨부되어 있다.)

 

이를 Promise로 해결했는데,

 

그렇다면 Promise에서는 어떻게 비동기 처리를 구현할까?

 

const fetchReturn = ()  =>
	new Promise((resolve, reject) => {
    	setTimeout(() => resolve(1), 1500);
});

const getHen = () => 
    new Promise((resolve, reject) => {
        setTimeout(() => resolve('닭'), 1000);
});

const getEgg = hen => 
    new Promise((resolve, reject) => {
        setTimeout(() => reject(new Error(`${hen} => 달걀`)), 1000);
});
    

fetchReturn()
	.then(num => console.log(num));

getHen()
	.then(hen => getEgg(hen))
    .then(egg => console.log(egg));

getHen()
	.then(hen => console.log(hen));

위 코드를 통해 간단한 비동기 코드를 이해해보자ㅏ.

 

정의는 건너뛰고 아래 실행 부분을 살펴보면,

1) fetchReturn은 1.5초 후 1을 출력한다.

2) (첫번째)getHen은 1초 후 '닭'을 전달하고, getEgg는 1초 후 '닭 => 달걀'을 출력한다.

(즉, 2초 후 '닭 => 달걀'을 출력한다.)

3) (두번째)getHen은 1초 후 '닭'을 출력한다.

 

Promise는 호출되는 순간 내부 로직을 백그라운드에서 실행시킨다고 이해했다.

그렇기에, 코드 순서가 아닌 처리 시간 순서대로

1

닭 => 달걀

이 출력된다고 이해했다.

 

즉, .then, .catch는 비동기를 이해하기 위함이 아니라

Promise의 작동 방식을 이해하기 위해서 공부한 것이고

 

Promise는 그 자체로 호출되면서부터 그 내부 로직이 비동기처리된다고 이해했다.

(then, catch만 죽어라 공부해도 비동기랑 연관성이 이해가 되지 않았더라는..)

 

 

Typescript 코드로 변환

위에서 사용한 코드를 TS로 변환해보았다.

 

// Promise is a JavaScript object for asynchronous operation.
// State: pending -> fulfilled or rejected
// Producer vs Consumer

// 1. Producer
// when new Promise is created, the executor runs automatically.
const testPromise = new Promise((resolve, reject) => {
    // doing some heavy work (network, read files)
    console.log('doing something...');
    setTimeout(() => {
        resolve('woojin');
        // reject(new Error('no network'));
    }, 500);
});

// 2. Consumers: then, catch, finally
testPromise
    .then(console.log)
    .catch(console.log);

// 3. Promise chaining
const fetchNumber: Promise<number> = new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000);
});

const fetchReturn = (): Promise<number>  =>  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1500);
});


fetchNumber
    .then(num => num * 2)
    .then(num => num * 3)
    .then(num => {
        return new Promise((resolve, reject) => {
            setTimeout(() => resolve(num - 1), 1000);
        });
    })
    .then(num => console.log(num));
    

// fetchReturn()
//     .then(num => num * 2)
//     .then(num => num * 3)
//     .then(num => {
//         return new Promise((resolve, reject) => {
//             setTimeout(() => resolve(num - 1), 1000);
//         });
//     })
//     .then(num => console.log(num));


// 4. Error Handling
const getHen = (): Promise<string> => 
    new Promise((resolve, reject) => {
        setTimeout(() => resolve('닭'), 1000);
    });

const getEgg = (hen: string): Promise<string> => 
    new Promise((resolve, reject) => {
        // setTimeout(() => resolve(`${hen} => 달걀`), 1000);
        setTimeout(() => reject(new Error(`${hen} => 달걀`)), 1000);
    });

const cook = (egg: string): Promise<string> => 
    new Promise((resolve, reject) => {
        setTimeout(() => resolve(`${egg} => 프라이`), 1000);
    });

getHen()//
.then(hen => getEgg(hen))
.catch(error => {
    return '밥';
})
.then(egg => cook(egg))
.then(meal => console.log(meal))
.catch(error => console.log(error));

 

// Callback Hell example

interface role {
    name: string;
    role: string;
}

const admin: role = {
    name: 'woojin',
    role: 'admin',
}

class UserStorage {
    loginUser(id: string, password: string): Promise<string> {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (
                    (id === 'woojin' && password === 'hustle') ||
                    (id === 'money' && password === 'many')
                ) {
                    resolve(id);
                } else {
                    reject(new Error('not found'));
                }
            }, 2000);
        });
    }

    getRoles(user: string): Promise<role> {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (user === 'woojin') {
                    // resolve({ name: 'woojin', role: 'admin' });
                    resolve(admin)
                } else {
                    reject(new Error('no access'));
                }
            }, 1000);
        });
    }
}

const userStorage = new UserStorage();
const id = 'woojin';
const password = 'hustle';

userStorage.loginUser(id, password)
    .then(id => userStorage.getRoles(id))
    .then(value => console.log(value))
    .catch(error => console.log(error));


// userStorage.loginUser(
//     id,
//     password,
//     user => {
//         userStorage.getRoles(
//             user,
//             userWithRole => {
//                 console.log(`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`);
//             },
//             error => {
//                 console.log(error);
//             }
//         );
//     },
//     error => {
//         console.log(error);
//     }
// );

 

 

다음 포스팅에서는 비동기 처리의 끝판왕,

async, await에 대해서 다뤄보겠다!

 

 

 

반응형
반응형

 

헷갈리는 동기, 비동기 개념을 정리하는 포스팅 (1)

 

첫번째 포스팅에서는,

callback이 무엇인지,

어떻게 쓰이는지,

그리고 화살표 함수를 제대로 해석하는 방법까지 정리해보고자 한다.

 

참고한 영상 : https://youtu.be/s1vpVCrT8f4

 


callback이란?

1. 다른 함수(또는 코드)의 인자로서 활용되는 함수.

2. 어떤 이벤트에 의해 호출되는 함수.

 

쉽게 말하면, 함수의 인자처럼 전달되어서 쓰이는 함수를 의미한다.

 

const print = () => console.log('hi');

setTimeout(print, 2000)

print는 아무 인자도 전달받지 않고 'hi'를 출력하는 함수이다.

setTimeout은 함수 하나(print)와 숫자(ms)를 전달받아서,

숫자 만큼의 시간이 지난 후 함수(print)를 호출하는 함수이다.

 

여기서 이 print라는 함수가 callback이 된다.

setTimeout이라는 함수의 인자로 쓰여서, 2000ms가 지나는 이벤트 후에 호출되었기 때문이다.

이렇게 callback이라는 것은 (아주 비약하자면) 함수의 인자처럼 쓰이는 함수이다.

 

 

callback함수의 활용과 callback지옥 (화살표 함수에 대한 이해)

실행결과는 아래와 같다

실행 2초 후 위와 같은 문자열이 출력된다.

 

'woojin', 'hustle'이라는 id와 password 그리고

onSuccess(user를 넣으면 getRoles에 user를 전달해서 실행하는 함수)

onError(error를 넣으면 error를 출력하는 함수)

이렇게 4개의 인자를 loginUser에 전달해서 실행시킨다.

 

여기서 onSuccess, onError에서 정의한 함수가 callback이다.

 

onError는 loginUser에 전달되는데, 

loginUser에서는 onError에 'not found'라는 문자열을 출력하는 new Error 객체를 전달하여 실행시키게 되어 있다.

 

관계가 복잡하지만,

화살표 함수의 용법과 콜백의 정의, 쓰임에 대해 정확하게 이해할 수 있는 코드이다.

 

하지만, 저렇게 해석하기 어려운 코드는 이해하기에도, 유지보수에도 불리하다.

 

위처럼 콜백이 콜백을 부르며 코드 왼쪽 공백의 깊이가 깊어지는 것을 콜백 지옥이라고 부른다.

 

다음 포스팅에서는, 콜백 지옥을 해결할 수 있는 방법인

Promise의 활용에 대해서 다뤄보겠다.

 

 

+) Javascript로 작성된 위 코드를 Typescript로 변환해보았다.

// Callback Hell example

interface success {
    name: string;
    role: string;
}

const obj: success = {
    name: 'woojin',
    role: 'admin',
}

class UserStorage {
    loginUser(
        id: string,
        password: string,
        onSuccess: (x: string) => void,
        onError: (x: object) => void) {
        setTimeout((): void => {
            if (
                (id === 'woojin' && password === 'hustle') ||
                (id === 'money' && password === 'many')
            ) {
                onSuccess(id);
            } else {
                onError(new Error('not found'));
            }
        }, 2000);
    }

    getRoles(user: string, onSuccess: (x: success) => void, onError: (x: object) => void) {
        setTimeout(() => {
            if (user === 'woojin') {
                // onSuccess({ name: 'woojin', role: 'admin' });
                onSuccess(obj);
            } else {
                onError(new Error('no access'));
            }
        }, 1000);
    }
}

const userStorage = new UserStorage();
const id: string = 'woojin';
const password: string = 'hustle';
userStorage.loginUser(
    id,
    password,
    (user: string) => {
        userStorage.getRoles(
            user,
            (userWithRole: success) => {
                console.log('hi')
                console.log(`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`);
            },
            error => {
                console.log(error);
            }
        );
    },
    error => {
        console.log(error);
    }
);

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형

+ Recent posts