프라미스 체이닝(promise chaining)

result.then 핸들러의 체인을 통해 전달된다는 점에서 착안한 아이디어 

 

예시

- fetch 이용

fetch('/article/promise-chaining/user.json')
  // 원격 서버가 응답하면 .then 아래 코드가 실행됩니다.
  .then(function(response) {
    // response.text()는 응답 텍스트 전체가 다운로드되면
    // 응답 텍스트를 새로운 이행 프라미스를 만들고, 이를 반환합니다.
    return response.text();
  })
  .then(function(text) {
    // 원격에서 받아온 파일의 내용
    alert(text); // {"name": "Violet-Bora-Lee", "isAdmin": true}
  });

 

-- 간단하게 변경

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => alert(user.name));

 

- fetch  + 함수

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise(function(resolve, reject) { // (*)
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser); // (**)
    }, 3000);
  }))
  // 3초 후 동작함
  .then(githubUser => alert(`${githubUser.name}의 이미지를 성공적으로 출력하였습니다.`))

-- 간략화 ( 함수단위로 분해)

function loadJson(url) {
  return fetch(url)
    .then(response => response.json());
}

function loadGithubUser(name) {
  return fetch(`https://api.github.com/users/${name}`)
    .then(response => response.json());
}

function showAvatar(githubUser) {
  return new Promise(function(resolve, reject) {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  });
}

// 함수를 이용하여 다시 동일 작업 수행
loadJson('/article/promise-chaining/user.json')
  .then(user => loadGithubUser(user.name))
  .then(showAvatar)
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));
  // ...

 

참고자료

https://ko.javascript.info/async

프라미스 

promise 객체 기본 구조

let promise = new Promise(function(resolve, reject) {
  // executor (제작 코드)
});

  • executer 는 자동으로 실행, 이 부분에서 코드로 작성한 원하는 작업이 처리됩니다.
  • executer 의 인수 resolve 와 reject 는 자바스크립트에서 자제 제공하는 콜백입니다.
    • resolve : 작업이 성공적으로 끝난 경우 그 결과를 나타낸는 value 와 함께 호출됩니다.
    • reject : 에러 발생시 에러객체를 나타내는 error와 함께 호출
  • 프로미스의 특징 : 성공또는 실패 둘 중 하나만 존재

예시

let promise = new Promise(function(resolve, reject) {
  resolve("완료");

  reject(new Error("…")); // 무시됨
  setTimeout(() => resolve("…")); // 무시됨
});

 

프라미스 소비자  then, catch, finally

then

- 첫 번째 인수는 프라미스가 fullfilled 되었을 때 실행 -> 실행 결과

- 두 번째 인수는 프라미스가 refuse 되었을 때 실행 -> 에러 호출

promise.then(
  function(result) { /* 결과(result)를 다룹니다 */ },
  function(error) { /* 에러(error)를 다룹니다 */ }
);

 

reject 예시

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

// reject 함수는 .then의 두 번째 함수를 실행합니다.
promise.then(
  result => alert(result), // 실행되지 않음
  error => alert(error) // 1초 후 "Error: 에러 발생!"을 출력
);

 

resolve 예시

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("완료!"), 1000);
});

// resolve 함수는 .then의 첫 번째 함수(인수)를 실행합니다.
promise.then(
  result => alert(result), // 1초 후 "완료!"를 출력
  error => alert(error) // 실행되지 않음
);

 

Catch

에러만 다루고 싶은 경우 사용 try {...} catch{...} 에서 catch 와 같은 역할

.then() 을 다 나열한 후 마지막에 사용되는 소비자

 

Finally

결과가 어떻든 마무리가 필요하면 finally 가 유용

finally 에선 절차를 마무리하는 보편적 동작을 수행하기 때문에  성공 혹은 실패 여부를 몰라도 됨

finally 는 자동으로 다음 핸들러에 결과와 에러를 전달

 

 

참고자료

https://ko.javascript.info/async

예시로 mysql 을 사용

 

  • Loaded:  명시한 서비스가 활성화된 위치를 나타냅니다.
    • enabled: 서비스가 시스템 부팅 시 자동으로 시작되도록 설정되어 있음을 의미합니다.
    • preset: enabled : 서비스가 기본적으로 활성화 상태로 설정되어 있다는 것을 의미합니다.
  • Active:  서비스가 실행된 날짜와 시간이 적혀 있으며, 실행된 시점 부터 지난 시간을 알려 줍니다.
  • Process: 서비스를 시작하기전에 수행되는 명령어를 보여줍니다.
    • 7117 : 명령어를 실행하는데 사용되는 스크립트나 명령어의 프로세스 아이디를 의미합니다.
    • ExecStartPre : 서비스가 실제로 시작되기 전에 실행되어야 하는 스크립트나 명령어를 지정합니다.
    • Code-Exited, Status=0/SUCCESS : ExecStartPre에 있는 명령어가 성공적으로 수행되었다는 것을 의미합니다.
  • Main PID: mysql 의 PID
  • Status: 서비스의 상태를 나타냅니다.
  • Tasks:  이 서비스가 관리하는 작업(쓰레드)의 수는 38개 이고, 제한은 1130개 라는 것을 의미합니다.
  • Memory: 현제 이 서비스가 사용하고 있는 메모리의 양을 나타냅니다.
  • CPU: CPU가 소모된 시간을 나타냅니다.
  • CGroup: control group (cgroup),  systemd가 MySQL 서비스 리소스를 관리하는 데 사용하는 제어 그룹(cgroup)으로, 특히 주 PID와 서비스 실행 파일에 연결합니다. MySQL 가 어떤 자원을 어느 정도 사용하고 있는지 보여주는 경로를 제공합니다

 

애플리케이션의 설정은 배포시 상황 ( 프로덕션, 개발, 로컬)에 따라 달라질 수 있습니다.

이때 환경변수를 상황에 맞게 저장을 해두는 것이 좋습니다.

.env (.env.local, .env.dev, .env.prod) 라는 곳에 저장된 변수들을 불러오는 방법은 아래와 같습니다.

 

설치

$ npm i --save @nestjs/config

 

예시

//package.json
{
  ...
  "scripts": {
    ...
    "start:local": "NODE_ENV=local nest start --watch", 
    ...
  },
  ...
}
// app.module.ts

import * as dotenv from 'dotenv';
dotenv.config();
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as Joi from 'joi';
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';

@Module({
  imports: [
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('dev', 'prod', 'local', 'debug').default('local'),
      }),
      isGlobal: true,
      envFilePath: `.env.${process.env.NODE_ENV}`,
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'mysql',
        host: configService.get<string>('DB_HOST'),
        port: configService.get<number>('DB_HOST'),
        username: configService.get<string>('DB_USERNAME'),
        password: configService.get<string>('DB_PASSWORD'),
        database: configService.get<string>('DB_NAME'),
        autoLoadEntities: true,
        synchronize: true,
        logging: true,
        namingStrategy: new SnakeNamingStrategy(),
      }),
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
// main.ts

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const configService = app.get(ConfigService);
  // .env.local 에 있는 변수 호출
  await app.listen(configService.get<number>('PORT'), configService.get<string>('HOST_IP'));
  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

 

터미널에서 실행 방법

$ npm run start:local

콜백

setTimeout은 스케줄링에 사용되는 가장 대표적인 함수

스크립트나 모듈을 로딩하는 것 또한 비동기 동작

 

콜백 기반(callback-based)’ 비동기 프로그래밍

<예시>

  • 스크립트 읽어오는 함수
function loadScript(src) {
  // <script> 태그를 만들고 페이지에 태그를 추가합니다.
  // 태그가 페이지에 추가되면 src에 있는 스크립트를 로딩하고 실행합니다.
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}
  • 스크립트 로딩이 끝나자마자 이 스크립트를 사용해 무언가를 해야만 한다고 가정
// script.js엔 "function newFunction() {…}"이 있습니다.
loadScript('/my/script.js'); 

newFunction(); // 함수가 존재하지 않는다는 에러가 발생합니다!

     - 에러는 브라우저가 스크립트를 읽어올 수 있는 시간을 충분히 확보하지 못했기 때문에 발생

  • loadScript 의 두 번째 인수로 스크립트 로딩이 끝난 후 실행될 함수인 콜백함수를 추가
function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}
  • 새롭게 불러온 스크립트에 있는 함수를 콜백 함수 안에서 호출
loadScript('/my/script.js', function() {
  // 콜백 함수는 스크립트 로드가 끝나면 실행됩니다.
  newFunction(); // 이제 함수 호출이 제대로 동작합니다.
  ...
});

→ 원하는 대로 외부 스크립트 안의 함수를 사용가능

 

무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리된 후 실행되어야 하는 함수가 들어갈 콜백을 인수로 반드시 제공해야 합니다. 이렇게 콜백을 사용한 방식은 비동기 프로그래밍의 일반적인 접근법

 

콜백 속 콜백

 

스크립트가 두 개 있는 경우, 어떻게 하면 두 스크립트를 순차적으로 불러올 수 있을까요?→ 방법은 콜백 함수안에 loadScript 를 호출하는 것, 이렇게 중첩 콜백을 만들면 바깥에 위치한 loadScript가 완료된 후, 안쪽 loadScript가 실행

 

!!! 콜백 안에 콜백을 넣는 것은 수행하려는 동작이 많은 경우엔 좋지 않은 방식

loadScript('/my/script.js', function(script) {

  alert(`${script.src}을 로딩했습니다. 이젠, 다음 스크립트를 로딩합시다.`);

  loadScript('/my/script2.js', function(script) {
    alert(`두 번째 스크립트를 성공적으로 로딩했습니다.`);
  });

});

 

에러 핸들링

loadScript에서 로딩 에러를 추적할 수 있게 기능을 개선

이러한 방식으로 에러를 다루는 패턴을 ⇒ 오류 우선 콜백(error-first callback) 이라고 한다

<예시>

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`${src}를 불러오는 도중에 에러가 발생했습니다.`));

  document.head.append(script);
}

 

<사용방식>

loadScript('/my/script.js', function(error, script) {
  if (error) {
    // 에러 처리
  } else {
    // 스크립트 로딩이 성공적으로 끝남
  }
});

 

오류 우선 콜백 의 관례

  1. callback의 첫 번째 인수는 에러를 위해 남겨둡니다. 에러가 발생하면 이 인수를 이용해 callback(err)이 호출됩니다.
  2. 두 번째 인수(필요하면 인수를 더 추가할 수 있음)는 에러가 발생하지 않았을 때를 위해 남겨둡니다. 원하는 동작이 성공한 경우엔 callback(null, result1, result2...)이 호출됩니다.

⇒ 오류 우선 콜백 스타일을 사용하면, 단일 콜백 함수에서 에러 케이스와 성공 케이스 모두를 처리할 수 있습니다.

 

멸망의 피라미드

  • 꼬리에 꼬리를 무는 비동기 동작이 많아지면 깊은 중첩이 있는 코드가 만들어진다
  • 이렇게 깊은 중첩 코드가 만들어내는 패턴은 소위 ‘콜백 지옥(callback hell)’ 혹은 '멸망의 피라미드(pyramid of doom)'라고 불립니다
loadScript('1.js', function(error, script) {

  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // 모든 스크립트가 로딩된 후, 실행 흐름이 이어집니다. (*)
          }
        });

      }
    })
  }
});

 

각 동작을 독립적인 함수로 만들어 위와 같은 문제를 완화하는 방법?

loadScript('1.js', step1);

function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', step2);
  }
}

function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('3.js', step3);
  }
}

function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // 모든 스크립트가 로딩되면 다른 동작을 수행합니다. (*)
  }
};
  • 각 동작을 분리해 최상위 레벨의 함수로 만들었기 때문에 깊은 중첩이 없습니다.
  • 코드가 여기저기 흩어져 보이는 문제가 있다.
  • 게다가 step*이라고 명명한 함수들은 '멸망의 피라미드’를 피하려는 용도 만으로 만들었기 때문에 재사용이 불가능

→ 이를 해결할 수 있는 가장 좋은 방법 중 하나는 “프라미스” 를 사용하는 것이다.

 

참고자료

https://ko.javascript.info/async

+ Recent posts