|| : OR 연산자 (MDN)

Logical OR 

논리 OR 계산용

둘 중 하나이상 참이면 👉 참

 

  • true || false ⇒ true
  • true || true ⇒ true
  • false || false ⇒ false

 

 

?? : null 병합 연산자 (MDN)

왼쪽이 null 또는 undefined 이면 오른쪽을 반환, 아니면 왼쪽값 사용

 

 

UseCase

form 의 default 값을 지정하려한다

store 에 저장해둔 값이 있다면 사용하고,

없다면 기본값을 수동으로 지정하려 한다.

 

[Incorrect]

 defaultValues: {
      setBillingAddressAsShippingAddress: paymentForm?.setBillingAddressAsShippingAddress || true,
      isManual: paymentForm?.isManual || false, 
      useOfTerms: paymentForm?.useOfTerms || false, 
      marketing: paymentForm?.marketing || false,
    },

 

위 처럼 작성한 경우 OR 논리 연산을 하게 된다. 

paymentForm.setBillingAddressAsShippingAddress 값이 false 인데, 뒤의 true 논리 연산에 의해 -> true 로 덮여짐

 

 

[Correct]

defaultValues: {
      setBillingAddressAsShippingAddress: paymentForm?.setBillingAddressAsShippingAddress ?? true, 
      isManual: paymentForm?.isManual ?? false,
      useOfTerms: paymentForm?.useOfTerms ?? false,
      marketing: paymentForm?.marketing ?? false,
    },

 

store 에 저장해둔 값이 있다면 사용하고(왼쪽값 리턴),

없다면 기본값을 수동으로 지정하려 한다(오른쪽 값 리턴).

 

[현상황]

jwt accessToken / refreshToken 을 사용하여 로그인 검증을 진행하고 있음

API 요청시 400 Bad Request 와 같은 응답은 response 로 잘 받아지고 있으나, 

accessToken 이 만료된 후, API 요청을 했을 때 401 Unauthorized 응답, 근데 response 에 접근하지 못하고 try-catch 에서 catch 에러로 빠지며, 스크립트로, status code, error message 에 접근하지 못함.

그냥 "failed to Fetch" 메시지로 찍힘

 

 

[기대상황]

API 요청시 400 / 401 모두 백엔드 서버에서 응답하는 response 객체로, HTTP status code 와 에러메시지로 접근하여 에러를 핸들링하고,

네트워크 레벨 에러, CORS 에러만 catch 에러로 핸들링하고자 함

 

 

Axios Instance 처럼 사용하고 있는 custom fetch Instance 

[코드]

import { getCookie } from "cookies-next";
import { signIn, getSession } from "next-auth/react";
import { CustomError } from "./customError";

interface FetchOptions extends RequestInit {
  headers?: HeadersInit;
}

let lastSession: any = null;

const fetchWithToken = async (
  url: string,
  method: string = "GET",
  body: any = null,
  options: FetchOptions = {},
): Promise<Response | undefined> => {
  // 현재 들고 있는 최근 세션 없는 경우만 session 요청
  if (lastSession == null) {
    const session = await getSession();
    if (session) {
      lastSession = session;
    } else {
      // 세션이 없는 경우 - 로그인 안한 상태
      // throw new CustomError("Unauthorized", 401);
    }
  }

  // 세션 정보 있는 경우 api 콜
  const headers = {
    ...options.headers,
    "Content-Type": "application/json",
    Country: getCookie("country") || "us",
    Language: getCookie("language") || "en",
    Authorization: `Bearer ${(lastSession?.user as any).accessToken}`,
  };

  const fetchOptions: FetchOptions = {
    ...options,
    method,
    headers,
    credentials: "include",
  };

  if (body) {
    fetchOptions.body = JSON.stringify(body);
  }

  try {
    const response = await fetch(`${process.env.NEXT_PUBLIC_CHAT_SERVER_API_URL}${url}`, fetchOptions);
    console.log("fetch response : ", response);
    return response;
  } catch (error) {
    // Promise 자체가 rejected (network error, CORS 등)
    console.log("fetch catch error : ", error);
    throw error;
  }
};

export { fetchWithToken };

 

 

백엔드로부터 정상 응답 (에러 응답도 포함 400, 401, 404, 등) 을 받는다면, fetch 의 response 에 접근할 수 있으나,

 

 

백엔드에서 정상 에러 응답 처리한 400 에러,

응답헤더에 CORS 처리가 잘 되어있다

 

 

프론트에서 fetch 의 response 로 받아서 console.log 에 response 가 status 코드와 함께 잘 찍힘

 

 

백엔드에 요청조차 하지 못하거나 (network level error),  CORS 처리가 되지 않은 응답을 받는다면, 바로 catch 에러 구문으로 떨어지게 된다.

 

크롬 개발자 툴에서 네트워크 탭을 확인해 보면, 401 Unauthorized 라고 백엔드로부터 정상적인 에러응답을 받은것 처럼 보인다.

여기서 헷갈리기 쉬웠던 것이 CORS 에러도 401 에러로 찍힌다.

응답헤더도 확인해보면 CORS 처리가 안되어 있는 것을 확인할 수 있다.

 

실제로 catch 구문으로 떨어져서, 프론트의 스크립트 상으로 status code와 error 메시지에 접근할 수 없고, 그저 fetch 자체에서 실패 처리한 "Failed to fetch" 메시지만 확인할 수 있다.

 

 

[해결책]

백엔드에서 실제 데이터 관련한 응답에서 에러응답을 리턴할때는 CORS 처리가 잘되어있었는데,

JWT 만료관련된 에러 응답을 리턴할 때는 CORS 처리가 안되어있는 것을 확인했다.

백엔드에서 응답헤더에 CORS 처리를 해주면 해결!

 

 

[포인트]

CORS 에러랑, 백엔드와 약속한 토큰만료시 리턴할 에러코드가 동일하게 401 이다.

이 때문에 CORS 에러 때문에 catch 에러로 떨어진 다는 것을 빠르게 캐치하지 못했다.

흔하게 많이 사용되는 간단한 알고리즘(?)을 정리하려한다

 

다음과 같이 객체로 이루어진 리스트가 있다

const transactionList = [
        {
          id: 1,
          transactionId: 99,
          sellDate: "20220801",
          price: 300,
        },
        {
          id: 2,
          transactionId: 99,
          sellDate: "20220701",
          price: -200,
        },
        {
          id: 3,
          transactionId: 99,
          sellDate: "20221001",
          price: 400,
        },
        {
          id: 4,
          transactionId: 99,
          sellDate: "20221101",
          price: -100,
        },
      ];

 

 

[Case1] 여기서 sellDate 가 가장 빠른 날짜를 찾고 싶다

 

1. 리스트 안의 Object 에서 필요한 키(sellDate)에 해당하는 Value 들을 다 뽑아서 리스트로 구성한다

2. Math.min을 이용하여 최솟값을 리턴해준다

function findMin(key) {
	return Math.min(...transactionList.map((obj) => obj[key]));
}
const firstTradeDate = findMin("sellDate")


// result : 20220701

 

 

[Case2] 이번엔 sellDate가 가장 빠른 날짜 객체 통채로를 찾고 싶다.

reduce 메소드를 이용하여 해당 객체의 index를 가져오기로 한다

  const getFirstTargetIndex = (array, target) =>
    array.reduce(
      (prevIdx, value, curIdx, arr) =>
        value[target] >= arr[prevIdx][target] ? prevIdx : curIdx,
      0
    );

  const answer = getFirstTargetIndex(transactionList, "sellDate");
  console.log("answer", answer);
  
  // result : 1

 

reduce 메소드는 매개변수로 콜백함수와 초기값을 받는다

 

콜백함수

(prevIdx, value, curIdx, arr) =>
        value[target] >= arr[prevIdx][target] ? prevIdx : curIdx,

콜백함수는 (이전 index, 현재 object, 현재 index, 리스트) 를 매개변수로 받는다

여기서 이전 Index는 계속 갱신되는 값이다.

누산을 통해서 우리는 index 값을 리턴받기 때문에 index 값이 받아지는 것이다.

 

 

초기값

0

여기서 0은 직접 비교대상이 되는 숫자 0의 역할이 아니고, 초기 index 역할이다

arr[0] 번째의 값을 초기값으로 잡아서 비교를 시작한다

 

 

1. migrations 파일 & model 생성

sequelize model:generate --name Member --attributes name:string, password:string
  • DBMS에 적용하기 위한 migrations 파일이 만들어지고, ORM에서 객체로 사용할 memer.js 파일이 만들어진다

2. 생성된 migrations 파일에 필요한 컬럼 추가

"use strict";
module.exports = {
  async up(queryInterface, Sequelize) {
    await queryInterface.createTable(
      "members",
      {
        id: {
          allowNull: false,
          autoIncrement: true,
          primaryKey: true,
          type: Sequelize.INTEGER,
        },
        name: {
          type: Sequelize.STRING(50),
          comment: "실명",
        },
        password: {
          type: Sequelize.STRING(512),
          comment: "비밀번호",
        },
        email: {
          type: Sequelize.STRING(100),
          allowNull: false,
          comment: "이메일",
        },
        is_deleted: {
          type: Sequelize.TINYINT.UNSIGNED,
          defaultValue: 0,
          comment: "삭제여부(0:아니오, 1:예)",
        },
        phone: {
          type: Sequelize.STRING(256),
          allowNull: false,
          comment: "휴대폰(010-XXXX-XXXX)",
        },
        created_at: {
          type: "TIMESTAMP",
          defaultValue: Sequelize.fn("NOW"),
          allowNull: false,
          comment: "등록일시",
        },
        updated_at: {
          type: "TIMESTAMP",
          defaultValue: Sequelize.literal(
            "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
          ),
          comment: "수정일시",
        },
      },
      {
        uniqueKeys: {
          phone_unique: {
            fields: ["phone"],
          },
        },
        comment: "회원",
      }
    );
  },
  async down(queryInterface, Sequelize) {
    await queryInterface.dropTable("members");
  },
};

 

3-1. (수작업) 생성된 models/member.js 파일도 동일하게 column 작업

  • 생성한 migrations 파일과 동일하게 넣어준다

 

3-2. (반자동?) migrations 파일로 먼저 DB에 반영을 하고, 반영된 내용을 그대로 ORM Model로 가져온다 

 

  • models/member.js → sequelize-cli 명령어로 생성된 member 모델 파일 삭제
  • 작성한 migrations 파일 DBMS에 반영
npx sequeilize-cli db:migrate
  • sequelize-auto 설치
npm install -g -d sequelize-auto
  • sequelize-auto 실행 파일 작성
    • 프로젝트 루트 디렉토리에 /orm.js
const SequelizeAuto = require("sequelize-auto");

const auto = new SequelizeAuto("DB-SCHEMA", "root", "db-password", {
  host: "localhost",
  dialect: "mysql",
  directory: "./models",
  port: "db-port",
  caseModel: "c",
  caseFile: "c",
});

auto.run((err) => {
  if (err) throw err;
});
  1.  

 

  • node orm 파일 실행

 

  • orm - model을 자동으로 만들어준다 조금만 수정해서 사용하자
    • init-models.js 삭제
    • member.js → Member.js
      • 객체로 사용할 거라서 이름 변경하고,
      • 파일 수정
      • index에서 정의한 sequelize 객체 import
      • underscored : true 추가
import sequelize from "../index.js";
import { DataTypes } from "sequelize";

const Member = sequelize.define(
  "Member",
  {
    id: {
      autoIncrement: true,
      type: DataTypes.INTEGER,
      allowNull: false,
      primaryKey: true,
    },
    type: {
      type: DataTypes.TINYINT.UNSIGNED,
      allowNull: false,
      comment: "회원 유형(0:관리자, 1:일반)",
    },
    name: {
      type: DataTypes.STRING(50),
      allowNull: true,
      comment: "실명",
    },
    password: {
      type: DataTypes.STRING(512),
      allowNull: true,
      comment: "비밀번호",
    },
    email: {
      type: DataTypes.STRING(100),
      allowNull: false,
      comment: "이메일",
    },
    phone: {
      type: DataTypes.STRING(256),
      allowNull: false,
      comment: "휴대폰(010-XXXX-XXXX)",
      unique: "phone_unique",
    },
    nickname: {
      type: DataTypes.STRING(80),
      allowNull: true,
      comment: "닉네임",
    },
    is_deleted: {
      type: DataTypes.TINYINT.UNSIGNED,
      allowNull: true,
      defaultValue: 0,
      comment: "삭제여부(0:아니오, 1:예)",
    },
    portrait_url: {
      type: DataTypes.STRING(256),
      allowNull: true,
      comment: "프로필 사진 S3 url",
    },
    marketing_agree: {
      type: DataTypes.TINYINT.UNSIGNED,
      allowNull: true,
      defaultValue: 0,
      comment: "마케팅 수신 동의여부(0: 아니오, 1: 예)",
    },
    last_login_at: {
      type: DataTypes.DATE,
      allowNull: false,
      defaultValue: sequelize.literal("CURRENT_TIMESTAMP"),
      comment: "마지막 로그인 일시",
    },
    login_fail_count: {
      type: DataTypes.INTEGER,
      allowNull: false,
      defaultValue: 0,
      comment: "로그인 실패 횟수",
    },
    password_reset_code: {
      type: DataTypes.STRING(256),
      allowNull: true,
      comment: "패스워드 재설정 인증코드",
    },
    withdraw_at: {
      type: DataTypes.DATE,
      allowNull: true,
      comment: "탈퇴일시",
    },
  },
  {
    sequelize,
    tableName: "member",
    timestamps: true,
    indexes: [
      {
        name: "PRIMARY",
        unique: true,
        using: "BTREE",
        fields: [{ name: "id" }],
      },
      {
        name: "phone_unique",
        unique: true,
        using: "BTREE",
        fields: [{ name: "phone" }],
      },
    ],
    underscored: true,
  }
);

export default Member;

 

 

 

Node-Sequelize-Mysql (5) Jest 사용해서 만든 ORM 객체 테스트하기

 

 

 

 

1. MYSQL workbench 다운로드

https://dev.mysql.com/downloads/workbench/

 

MySQL :: Download MySQL Workbench

Select Operating System: Select Operating System… Microsoft Windows Ubuntu Linux Red Hat Enterprise Linux / Oracle Linux Fedora macOS Source Code Select OS Version: All Windows (x86, 64-bit) Recommended Download: Other Downloads: Windows (x86, 64-bit), M

dev.mysql.com

 

2. docker desktop 설치

 

3. docker 올리기

  • docker-compose.yml 작성 (.env와 같은 폴더 안)
services:
  mysql:
    platform: linux/amd64
    image: mysql:8.0.29-debian
    ports:
      - "${MYSQL_PORT}:3306"
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - "${MYSQL_DATA_DIR}:/var/lib/mysql"
    environment:
      MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"

 

  • .env 작성 (같은 폴더 안)
MYSQL_PORT=3306
MYSQL_DATA_DIR=/d/dev/mysql
MYSQL_ROOT_PASSWORD=hello123!

 

  • 로컬에 여러개 포트의 mysql을 띄우고 싶을 때
    • .env 파일안의 MYSQL_PORT를 달리해주면 된다
    • docker run -p 5000:3306 → 로컬에서 5000 보고 mysql용 3306 연결
    • docker run -p 7000:3306 → 로컬에 7000 포트에 또 mysql 띄울 수 있음
  • MYSQL_DATA_DIR = /d/dev/mysql
    • 도커를 올리면 mysql 폴더가 생성된다
    • 로컬에 여러개의 mysql을 올리고 싶을 때는 포트를 바꾸기
    • access denied 관련 에러가 나면 mysql 폴더를 지우고 도커를 다시 올려본다

  • docker-compose.yml 있는 폴더로 이동
docker compose up

 

  • MYSQL workbench 연결
    • 127.0.0.1
    • DB 이름 정하기
    • 비밀번호(.env 파일 작성 password)

 

  • CREATE SCHEMA

 

Node-Sequelize-Mysql (4) Model 생성 & migrations 파일 생성

 

 

 

1. mysql, sequelize, sequelize-cli 모듈 설치

$ npm i --save sequelize mysql2  // 모듈 설치
$ npm i -g sequelize-cli  // 콘솔에서 sequelize 명령 가능하게 하는 모듈
$ sequelize init  // 필요한 파일과 디렉토리를 자동 설치
  • 초기 세팅 폴더 및 파일 생성됨
    • 폴더 및 파일들의 간략한 설명만 하고 실제 세팅은 뒤에서 진행

  • config.json
    • sequelize 모듈을 DB 환경과 연결 시키는 용도
    • 편의상 js로 변경하여 사용한다
    export default {
      development: {
        username: "root",
        password: "hello1234!",
        database: "DEV-HELLO-WEB",
        host: "127.0.0.1",
        dialect: "mysql",
        port: 3307,
      },
      test: {
        username: "root",
        password: "hello1234!",
        database: "TEST-HELLO-WEB",
        host: "127.0.0.1",
        dialect: "mysql",
        port: 3307,
      },
      production: {
        username: "root",
        password: null,
        database: "HELLO-WEB",
        host: "127.0.0.1",
        dialect: "mysql",
        port: 3307,
      },
    };
  • models/~
    • ORM 객체로 사용할 Model을 정의하는 곳
    • RDB에서 member, post, qna 테이블들이 있다고 가정하면, 각각 테이블 별로 Member, Post, Qna 모델을 만들어 주고, 컬럼 속성들이나 제약조건들을 정의해 주면 된다
  • index.js
    • Sequelize 인스턴스를 생성
    • config에서 정의한 환경변수에 따라서 mysql DB와 연동된 sequelize 객체를 export
    • default로 만들어주는 내용을 아래와 같이 필요한 내용만 두고 수정한다
    "use strict";
    
    import { Sequelize } from "sequelize";
    import config from "./config/config.js";
    
    export default new Sequelize(config[process.env.NODE_ENV]);
    
  • migrations
    • RDB DDL 정의하는 파일들
    • 실제로 ORM 객체로 작성한 테이블 정의 파일을 Mysql DB에 테이블로 반영시킬 수 있다.
    • 이 migrations 파일들만 있으면, 어떤 RDBMS 에도 동일한 테이블 세팅을 구성할 수 있다

Node-Sequelize-Mysql (3) Mysql 로컬 db docker 띄우기

 

 

 

orm 사용해 봅시다

 

ORM 개념

  • ORM 이란
    • Object Relational Mapping : 객체-관계 매핑
    • 객체와 관계형 데이터베이스(RDB) 데이터를 자동으로 매핑해주는 툴
    • mysql로 tb_member이라는 테이블을 만들어서 데이터 관리를 한다
    • orm으로 Member 객체를 만들어서 sql 쿼리 없이 테이블을 객체처럼 관리한다
    • 객체지향 프로그래밍은 클래스(Class) 개념을 사용하는데, 중간중간 RDB의 테이블 개념이 이질적인 현상을 해소할 수 있다. → 전체적으로 객체지향(Class)로 프로그래밍이 가능
  • 장점
    • 객체 지향적인 코드로 전체적으로 직관적이고, 비즈니스 로직에 집중할 수 있게 해준다
    • 재사용 및 유지보수의 편리성이 증가한다
    • DBMS에 대한 종속성이 줄어든다 (DB 솔루션과 상관없다)
  • 단점
    • 프로그램의 복잡서이 커질 수록 ORM 난이도가 오히려 더 복잡해진다
    • 잘못 구현된 경우 속도 저하, 성능 저하, 일관성 무너짐 발생 가능

 

Node-Sequelize-Mysql (2) Sequelize 설치

 

 

 

비동기 프로그래밍에 대해서 정확하게 알지 못하고 늘 주변만 뱅뱅 돌면서 의미도 모르는 채로 기계처럼 사용해왔다.

 

회사에서 코드리뷰도 하고, 다른사람이 하던 작업을 넘겨 받아 리팩토링도 하면서 내가 그동안 얼마나 빈약한 개념을 가지고 있었는지 새삼 깨닫게 되었다.

 

당장 움직이지 않는 손가락이 너무 답답해서 이번에야 말로 비동기 프로그래밍을 이해해보자는 생각으로 정리를 해보았다.

 

Sync & Async

  • Sync
    • JS 는 Sync → 동기적으로 작동
    • hoisting이 된 이후부터 코드가 작성된 순서대로 실행된다
  • async
    • 비동기, 언제 코드가 끝날 지 모른다
    • 끝날 때까지 기다리지 않음
    • ex) setTimeout - callback 함수

 

Callback

  • Sync callback
console.log('1')

function printNow(print) {
	print()
)
printNow(() => console.log('hello')	

console.log('2')
  •  결과
1
hello
2
  • Async callback
console.log('1')
function printLater(print, delay) {
	setTimeout(print(), delay)
}
printLater(() => console.log('hello', 2)
console.log('2')
  •  결과
1
2
hello

⇒ 어쨌든 JS는 모든 코드에 똑같이 작동한다

  • 한 줄 한 줄 동기적으로
  • 실행할 함수 안에서 동기적으로 작동하게 하느냐, 비동기적으로 작동하게 하느냐를 구현하는 것
  • setTimeout 함수 자체가 시간이 걸리는 작업이라고 생각 → 사실상 JS는 동기적으로 실행을 순서에 맞게 시작했지만, 늦게 끝나서 도착한 결과만 놓고 봤을 때 코드 순서와 다를 뿐 (비동기적)

 

setTimeout 함수가 가지고 있는 콜백함수처럼 함수를 정의할 때 콜백함수를 정의 할 수 있다.

A함수 처리 하고 나서 B 함수 처리해 그리고 C 함수 처리해 그리고 D....

 

예제

1. 로그인 검사

2. user 정보를 가지고 권한 return

 

이런 일련의 과정들을 콜백지옥 체험 코드로 구현 해보자

class UserStorage {
  loginUser(id, password, onSuccess, onError) {
    setTimeout(() => {
      if (id == "apple" && password === "banana") {
        onSuccess(id);
      } else {
        onError(new Error("wrong user info"));
      }
    }, 2000);
  }

  getRoles(user, onSuccess, onError) {
    setTimeout(() => {
      if (user === "apple") {
        onSuccess({ id: "apple", role: "admin" });
      } else {
        onError(new Error("no role"));
      }
    }, 1000);
  }
}

const userStorage = new UserStorage();
const id = "apple";
const password = "banana";

userStorage.loginUser(
  id,
  password,
  (res) => {
    console.log("Welcom", res);
    userStorage.getRoles(res, (res) => {
      console.log(`hello ${res.id}, user Role is ${res.role}`);
    }),
      (error) => console.log("error", error);
  },
  (error) => console.log("error", error)
);
정의해둔 class와 메소드를 사용하여 로직을 구현하는 부분을 보면 가독성이 떨어진다.
콜백함수 안에 또 콜백함수가 들어가 있는 방식으로, 에러처리도 힘들고, 무엇을 진행하려고 하는 지 모르겠다

 

Promise

  • 프로미스는 JS 에서 제공하는 Class (Object)이다
  • 프로미스 Object 안에는 state라는 속성이 있다
    • pending : 진행중
    • fulfilled : 완료
    • rejected : 실패
  • Producer & Consumer
    • 필요한 정보를 만들어주는 Producer → Promise 객체
    • 그 정보를 소비하는 Consumer
  • Promise 생성자
    • executer 콜백 함수를 전달해 주어야 한다
      • executer 함수는 resolve와 reject 2가지의 콜백함수를 가지고 있다

  • Promise를 만드는 순간 내부의 콜백함수 (executer) 가 바로 실행된다
  • 사용자가 액션을 한 순간에 작업이 진행되어야 하면, Promise를 생성하는 방식으로 사용 불가 → 생성과 동시에 바로 실행되기 때문에 (유저의 액션을 체크 불가 & 불필요한 네트워크 통신)
const promise = new Promise((resolve, reject) => {
  console.log("Network connection");
});
  • Producer(resolve/reject) & Consumer(then/catch/finally)
  • Promise 에서 성공시 객체를 resolve에 담고, 실패시 에러를 reject에 담는다
  • 생성한 promise 객체에서는 then을 통해 성공에 대한 결과를 받아내고, catch를 통해 에러를 받아낸다
// Producer
const promise = new Promise((resolve, reject) => {
  console.log("Network connection");
  setTimeout(() => {
    result = "apple";
    if (result === "apple") resolve(result);
    else reject(new Error("no network"));
  }, 2000);
});

//Consumer
promise
  .then((res) => {
    console.log(res);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log("finally");
  });
  • then/catch/finally 메소드는 각각 모두 Promise를 반환 → Chaining 적용하여 처리
// Promise Chaining
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));
  • then을 통해서 값을 return 하거나, 새로운 Promise를 return 하거나
  • Promise Object 에 매개변수를 넘겨줄 수 있다
const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve("hen"), 1000);
  });

const getEgg = (hen) =>
  new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error("no Egg!")), 1000);
  });

const cook = (egg) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} -> fry`), 1000);
  });

getHen()
  .then((res) => getEgg(res))
  .catch((error) => {
    console.log(error);
    return "bread";
  })
  .then((res) => cook(res))
  .then((res) => console.log(res));

// 한가지만 받아서 그대로 넘겨줄 때 축약표현 가능
// getHen()
// .then(getEgg)
// .then(cook)
// .then(console.log);
  • 결과
// CASE1
hen -> egg -> fry

// CASE2
Error: no Egg!
    at Timeout._onTimeout (D:\dev\yakbang_front\promise.js:45:29)
    at listOnTimeout (node:internal/timers:559:17)
    at processTimers (node:internal/timers:502:7)
bread -> fry

 

예제

위에서 작성했던 콜백지옥을 Promise를 사용하여 레벨업을 시켜보자

class UserStorage {
  loginUser(id, password) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (id === "apple" && password === "banana") {
          resolve(id);
        } else {
          reject(new Error("wrong user info"));
        }
      });
    });
  }
  getRoles(user) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (user === "apple") {
          resolve({ id: "apple", role: "admin" });
        } else {
          reject(new Error("no role"));
        }
      });
    });
  }
}

const userStorage = new UserStorage();
const id = "apple";
const password = "banana";

userStorage
  .loginUser(id, password)
  .then((id) => userStorage.getRoles(id))
  .then((user) => console.log(`Welcom ${user.id} Role : ${user.role}`))
  .catch((error) => console.log(error));
이제 구현부의 내용을 보면, 순차적으로 실행되는 로직이 눈에 들어온다

async-await

  • Promise 를 간결하게 표현, 동기적으로 실행되는 것 처럼 표현
  • Promise도 좋은데, 좀 더 좋은 방법 없을까? -> 갑자기 new Promise 등장하는게 읽기 불편해
  • syntactic sugar : 코드상 슈가 → 기존에 있는 기술을 좀 더 편하게 사용할 수 있도록 하는 것
  • 모든 Promise를 대체하는 건 아니다
  • 동기적인 실행
    • apple을 받아오는데 2초가 걸리는데, 다 기다렸다가 Next Content 실행
function sleep(ms) {
  const wakeUpTime = Date.now() + ms;
  while (Date.now() < wakeUpTime) {}
}

function fetchUser() {
  sleep(2000);
  return "apple";
}

const user = fetchUser();
console.log("user", user);

console.log("Next Content");

-> 2초 뒤 콘솔로그

  • Promise 로 비동기적 실행 구현 (뒤에 관련 없는 Next Content를 먼저 표현한다)
function sleep(ms) {
  const wakeUpTime = Date.now() + ms;
  while (Date.now() < wakeUpTime) {}
}

function fetchUser() {
  return new Promise((resolve, reject) => {
    sleep(2000);
    resolve("apple");
  });
}

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

console.log("Next Content");

 

  • 함수 앞에 “async”를 붙여주면 자동으로 Promise로 return 해준다
    • Producer (Promise를 리턴하는 부분) 에서 Promise를 간결하게 표현하는 방법
function fetchUser() {
  return new Promise((resolve, reject) => {
    sleep(2000);
    resolve("apple");
  });
}

// 위와 아래는 똑같은 결과를 준다
async function fetchUser() {
  sleep(2000);
  return "apple";
}
  • “await”은 “async”가 붙은 함수 안에서만 사용 가능
    • “async” (Promise) 안에서 동기적으로(다 할때까지 기다려) 코드가 실행되도록 하는 장치
  • await 없이 delay 함수 실행하면? 
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchUser() {
  delay(2000);
  return "apple";
}

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

console.log("Next Content");

  • 바로 실행된다
  • why? fetchUser() 함수 안에서 delay를 기다리지 않고, 바로 apple을 return 해줌

 

  • awiat delay 주면 ? 
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchUser() {
  await delay(2000);
  return "apple";
}

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

console.log("Next Content")

-> 2초 뒤에 

  • 2초 동안 기다렸다가 apple을 리턴해줌
  • Next content가 먼저 표현되고, apple을 2초 뒤에 표현

 

 

  • async-await을 통해서 코드를 동기적으로 표현 
  • 가독성이 좋아지고 에러처리도 간편하다 try-catch
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchUser() {
  await delay(2000);
  console.log("2초 뒤");
  return "apple";
}

async function fetchRole() {
  await delay(1000);
  console.log("1초 뒤");
  return "admin";
}

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchUser() {
  await delay(2000);
  console.log("2초 뒤");
  return "apple";
}

async function fetchRole() {
  await delay(1000);
  console.log("1초 뒤");
  return "admin";
}

async function fetchUserAndRole() {
  try {
    const user = await fetchUser();
    const role = await fetchRole();
    return `${user} & ${role}`;
  } catch (error) {
    console.log(error);
  }
}

fetchUserAndRole().then(console.log);
console.log("Next Content");
  • 단점) 만약 user & role이 서로 연관 없는 데이터를 가져오는 행동이라면 불필요한 시간 소모가 된다
  • -> 2초 user 가져오고 난 후에 1초 role을 가져와서 총 3초가 걸린다
  • -> user 가져옴과 동시에 role을 병렬적으로 가져오면 총 2초로 단축 가능

 

 

  • 해결책
    • Promise는 생성과 동시에 실행되는 성격을 이용하자
async function fetchUserAndRole() {
  const userPromise = fetchUser(); // promise 만들자 마자 실행
  const rolePromise = fetchRole(); // promise 만들자 마자 실행
  const user = await userPromise;
  const role = await rolePromise;
  return `${user} & ${role}`;
}
  • Better Expression
    • 모든 promise를 한 줄 씩 각각 쓰지말고 -> Promise.all로 전체 실행 가능\
return Promise.all([fetchUser(), fetchRole()]).then((res) => res.join(" & "));
  • Another api
    • 실행하는 Promise 중에서 먼저 완료되는 것만 출력 -> Promise.race
async function fetchOne() {
  return Promise.race([fetchUser(), fetchRole()]);
}
fetchOne().then(console.log);
console.log("Next Content");
  • 단, 아예 실행이 안되는것이아니라, 내부적으로 콘솔로그나 로직은 모두 실행이 되는데, 최종 return 값만 가장 빨리 되는 놈으로 가져오는 것임!

 

 

예제

위에서 작성했던 콜백지옥 -> Promise를 최종적으로 Async-await을 이용해 작성해보자

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

  async loginUser(id, password) {
    await this.delay(2000);
    if (id === "apple" && password === "banana") {
      return id;
    } else {
      throw "wrong user Info";
    }
  }
  async getRoles(user) {
    await this.delay(1000);
    if (user === "apple") {
      return { id: "apple", role: "admin" };
    } else {
      throw "no role";
    }
  }
}

const userStorage = new UserStorage();
const id = "apple";
const password = "banana";

async function fetchUserAndRole(id, password) {
  const user = await userStorage.loginUser(id, password);
  const userWithRole = await userStorage.getRoles(user);
  return `Welcome ${userWithRole.id}, Role : ${userWithRole.role}`;
}

fetchUserAndRole(id, password).then(console.log);
가장 가독성이 좋다
따로 try-catch로 에러처리는 하지 않았지만, Class 내부 method에서 예외발생시 throw로 던져주고, 최종 구현부(fetchUserAndRole)에서 try - catch로 감싸면 메소드들에서 던진 예외상황들이 catch로 다 모여서 걸러진다

데이터타입

 

원시값

String, Number, Boolean, Null, Undefined

-> 자동 깊은 복사, 다른 메모리에 값 자체를 할당

 

참조값

Object, Symbol

-> 얕은 복사, 가리키는 주소를 복사

 

얕은 복사

객체를 가리키는 주소를 같이쓰는것

바꾸면 같이 따라서 바뀌어 버린다

 

깊은 복사

  • spread 연산자 사용 (1 depth 까지만)
  • Object.assign() 메소드 사용 (1 depth 까지만)

 

완벽한 깊은 복사 방법

  • 재귀적으로 깊은 복사 수행
  • Lodash의 cloneDeep 함수 사용 (라이브러리)
  • JSON.parse()와 JSON.Stringify() 함수 사용
    • JSON.stringify()는 객체를 json문자열로 변환한다. 이 과정에서 원본객체와 참조가 모두 끊어진다.
    • JSON.stringify()로 관계를 끊어버리고, 다시 JSON.parse()하여 객체형태로 만든다.
    • 단점 ; 느림
const obj = {
  a: 1,
  b: {
    c: 2,
  },
};

const copiedObj = JSON.parse(JSON.stringify(obj));

copiedObj.b.c = 3

obj.b.c === copiedObj.b.c //false

Javascript 에서는 객체를 상속하기 위해 프로토타입이라는 방식을 사용한다.

이미 존재하는 생성자에게 메소드를 추가하기 위해 프로토타입 속성을 사용하는 법을 알아보자

 

자바스크립트는 프로토타입기반 언어라고도 불리운다

모든 객체들이 그들의 프로토타입으로 부터 메소드와 속성들을 상속받는다.

모든 객체들은 최소한 하나 이상의 다른 객체로부터 상속을 받는데, 이때 상속되는 정보를 제공하는 객체를 프로토타입이라고 한다.

 

 

동작 순서 : 프로토타입 체인

person 객체가 자체적으로 valueOf() 메소드를 가지고 있는지 체크

person의 프로토타입 객체 Person()의 생성자의 프로토타입에 valueOf() 메소드가 있는지 체크

여전히 없으므로 Person()의 생성자의 프로토타입 객체의 프로토타입 Object() 생성자의 프로토타입이 valueOf()가 있는지 체크

 

Person 자체의 prototype은 따로 지정한게없어서 내용이 없음

하지만 Person의 부모 Object의 Prototype은 내용이 많다

이를 상속 받는다

 

 

생성자의 프로토타입에 메소드를 추가하면 생성한 인스턴스들에서 모두 사용할 수 있다

 

 

일반적으로 속성은 생성자에서, 메소드는 프로토타입에서 정의한다

 

 

프로토타입 = { 프로토타입 오브젝트 + 프로토타입 링크}

 

1. 프로토타입 오브젝트

객체는 항상 함수로 만들어진다 

var person = new Person()

 

함수가 정의될 때는 2가지 일이 동시에 일어난다

1) 해당 함수에 Constructor(생성자) 자격부여

2) 해당 함수의 "Prototype Object" 생성 및 연결 => prototype 속성이 생긴다

 

2. 프로토타입 링크

__proto__ : 객체가 생성될때 조상이었던 함수의 Prototype Object를 가리킨다.

프로토타입 체인 : 연결 연결 조상까지 올라가서 필요한 값을 찾는것

 

+ Recent posts