개인노트-인강

개인노트 정리 Node.js 백엔드 part 1-4

roroism 2023. 3. 26. 17:41

한 번에 끝내는 Node.js 웹 프로그래밍 초격차 패키지 백엔드 part 1

<모던 JavaScript 스타일>

  1. 모던 JavaScript - functional approach (1)
  2. 모던 JavaScript - functional approach (2)
  3. 모던 JavaScript - Promise
  4. 모던 JavaScript - polyfill, transpile

 

# 모던 JavaScript - functional approach (1)

  • 보통 functional chaining 같은 말들을 많이 하게 됩니다.
  • JavaScript를 사용할 때 함수 자체가 객체로 취급 될 수 있다 보니까 함수 자체를 인자로 넣는 등 함수를 잘 활용할 수 있는 언어이다 보니 함수형 언어로 보는 사람들이 많습니다. 그래서 functional approach를 많이 사용을 하게 됩니다.

이런 functional approach 방식으로 문제를 푸는 방식을 살펴보겠습니다.

// @ts-check
/* eslint-disable no-restricted-syntax */
/**
 * @typedef Person
 * 
 * @property {number} age
 * @property {string} city
 * @property {string | string[]} [pet]
* */

/** @type {Person[]} */
const people = [
    {
        age: 20,
        city: '서울',
        pet: ['cat', 'dog'],
    },
    {
        age: 40,
        city: '부산',
    },
    {
        age: 31,
        city: '대구',
        pet: ['cat', 'dog'],
    },
    {
        age: 36,
        city: '서울',
    },
    {
        age: 27,
        city: '부산',
        pet: 'cat',
    },
    {
        age: 24,
        city: '서울',
        pet: 'dog',
    },
]

/**
 * 다음 문제들을 풀어봅시다.
 *
 * A. 30대 미만이 한 명이라도 사는 모든 도시
 * B. 각 도시별로 개와 고양이를 키우는 사람의 수
*/

위 문제들을 2가지 방식으로 풀어볼 것입니다.첫번째 방식은 고전적인 방식인 for 루프를 사용합니다. 특별한 functional approach를 사용하지 않습니다.두번째 방식은 함수형 적인 방법으로..최대한 JavaScript의 유틸리티 함수들을 적극적으로 활용하면서 풀어봅니다.

 

1. 첫번째 방식

function solveA() {
    /** @type {string[]} */
    const cities = []
    
    for (const person of people) {
        if (person.age < 30) {
            if (!cities.find(city => person.city === city)) {
                cities.push(person.city)
            }
        }
    }
    
    return cities
}

console.log('solveA', solveA());

 

2. 두번째 방식

function solveModern() {
    const allCities = people.filter(person => person.age < 30).map(person => person.city);
    const set = new Set(allCities);
    return Array.from(set);
}

console.log('solveAModern', solveModern());

 

  • 두번째 방식이 더 깔끔해보이긴 합니다. 그리고 두번째 방식은 mutation (변형)을 사용하지 않는 다는 점이 제일 큰 장점으로 보입니다.
  • 두번째 방식에서 set 라인에서 윗 줄인 allCities 에서 무슨 일이 일어났는지는 크게 신경을 안써도 됩니다. 하지만 첫번째 방식에서는 cities로 선언한 변수를 계속 변형시켜가면서 작업을 하게 됩니다. 그러면 그 변형을 계속 머리속에서 트래킹을 해야 하기 때문에 아래쪽에서 현 상태를 가정하기가 어렵습니다. 지금 같은 예제에서는 단순하기 때문에 유추하기가 크게 어렵진 않지만, 문제가 복잡해질수록 이런 mutation에 의한 문제들이 많이 발생하기 때문에 두번째 방식처럼 mutation이 일어나지 않는 방법을 선호합니다.
  • 그리고 if나 for 같은 block을 사용하지 않고도 array 메소드만을 가지고 문제를 잘 해결할 수 있습니다.
  • array 메소드들은 if나 for 보다는 더 명료한 뜻을 가지고 있기 때문에 어떤 일이 일어날지 좀 더 명확하게 알 수 있습니다. 특히 filter나 map같은 경우가 그렇습니다.

위 두번째 방법의 syntax을 조금 더 정리해 본다면..

function solveModern() {
    const allCities = people.filter({ age } => age < 30).map({ city } => city);
    const set = new Set(allCities);
    return Array.from(set);
}

console.log('solveAModern', solveModern());

위처럼 object destructuring을 활용하면 더 깔끔하게 바꿀 수 있습니다.

 

# 모던 JavaScript - functional approach (2)

위 코드에서 B 문제를 풀어보겠습니다.

 

1. 첫 번째 방식

 // B. 각 도시별로 개와 고양이를 키우는 사람의 수
 
 /**
  * 예상되는 개와 고양이 수.
  * '서울': {
  *     'dog': 2,
  *     'cat': 1,
  * },
  * '대구': {
  *     'dog': 1,
  *     'cat': 1,
  * },
  * '부산': {
  *     'cat':1,
  * }
 */
 
 /** @typedef {Object.<string, Object.<string, number>>} PetsOfCities */
 function solveB() {
     /** @type {PetsOfCities} */
     const result = {}
     
     for (const person of people) {
         const { city, pet: petOrPets } = person
         
         if (petOrPets) {
             const petsOfCity = result[city] || {}
         
             if (typeof petOrPets === 'string') {
                 const pet = petOrPets
                 
                 const origNumPetsOfCity = petsOfCity[pet] || 0
                 petsOfCity[pet] = origNumPetsOfCity + 1
             } else {
                 for (const pet of petOrPets) {
                     const origNumPetsOfCity = petsOfCity[pet] || 0
                     petsOfCity[pet] = origNumPetsOfCity + 1
                 }
             }
             
             result[city] = petsOfCity
         }
     }
     
     return result
 }
 
console.log('solveB', solveB());
/*
{
    '서울': { cat: 1, dog: 2 },
    '대구': { cat: 1, dog: 1 },
    '부산': { cat: 1 }
}
*/

 

2. 두 번째 방식

 // B. 각 도시별로 개와 고양이를 키우는 사람의 수

// 여러가지 분기를 가질 수 있는 petOrPets를 하나로 통일하면 쉬울 것 같습니다.
// 현재로서는 해당 필드가 아예 없거나, string 이거나 array일 수 있습니다.
// 그래서 array로 통일하면 좋을 것 같습니다. 필드가 없다면 빈 배열 string은 원소 하나있는 배열로 치환.

/**
 * reduce 전 만드려는 배열
 * [
 *     ['서울', 'cat'],
 *     ['서울', 'dog'],
 *     ['부산', 'dog'],
 * ]
*/
function solveBModern() {
    return people.map(({ pet: petOrPets, city }) => {
        const pets = (typeof petOrPets === 'string' ? [petOrPets] : petOrPets) || []
        
        return { city, pets }
    /**
     * [
     *     [
     *         ['서울', 'cat'],
     *         ['서울', 'dog'],
     *     ],
     *     [
     *         ['부산', 'dog'],
     *     ]
     * ],
    */
    }).flatMap(({ city, pets }) => pets.map(pet => [city, pet]))
    .reduce((/** @type {PetsOfCities} */result, [city, pet]) => , {
        if (!city || !pet) result; // typescript를 만족시키기 위해 작성. 사실 undefined가 올 일은 없음.
        
        return {
            ...result,
            [city]: {
                ...result[city],
                [pet]: (result[city]?.[pet] || 0) + 1,
            }
        }
    })
}

console.log('solveBModern', solveBModern());
/*
{
    '서울': { cat: 1, dog: 2 },
    '대구': { cat: 1, dog: 1 },
    '부산': { cat: 1 }
}
*/
  • .map().flat() 을 flatMap() 으로 바꿀 수 있습니다.
  • reduce에서 spread operator를 사용하여 return. 이를 이용하여 객체에도 계속 누적하여 추가해 나갈 수 있습니다.
  • optional chaining을 이용하여 undefined 또는 null 일 경우 그대로 undefined 을 반환.

 

## 정리

첫번째 방식이 두번째 방식보다 성능적으로는 더 나을 수도 있습니다. 하지만 그 성능적 이점이 매우 근소합니다. 그래서 너무 이른 상태에서 최적화를 하지 말고, 일단 코드의 퀄리티에 집중하는 것이 더 좋을 것 같습니다.

 

# 모던 JavaScript - Promise

Promise에 대해서는 Web Docs에 자세하게 설명되어 있습니다.

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise

 

비동기적인 처리를 할 때  보통 callback으로 작성하게 되는데 이 경우, 콜백 헬과 같은 가독성 문제가 발생하고, 안쪽 scope일 수록 참조할 수 있는 바깥쪽 함수들이 점점 늘어나므로 실수할 가능성도 높아집니다.

그래서 이런 문제들 때문에 Promise나 async await를 이용하여 해결하게 됩니다.

 

## catch에 영향을 미치는 catch의 순서

.catch는 순서에 영향을 받습니다. Promise chain상에서 catch에서 가장 가까이 있는 에러를 잡아줍니다.

 

Promise(reject) -> then -> catch의 경우

new Promise((resolve, reject) => {
  console.log('Inside promise')
  reject(new Error('First reject'))
  resolve('First resolve')
})
  .then((value) => {
    console.log('Inside first then')
    console.log('value', value)
  })
  .catch((error) => {
    console.log('error', error)
  })
  
  // 실행결과
  // Error: First reject

 

Promise(reject) -> catch-> then의 경우

new Promise((resolve, reject) => {
  console.log('Inside promise')
  reject(new Error('First reject'))
  resolve('First resolve')
})
  .catch((error) => {
    console.log('error', error)
  })
  .then((value) => { // resolve 된 결과값이 없기 때문에 undefined
    console.log('Inside first then')
    console.log('value', value)
  })
  
  // 실행결과
  // Error: First reject
  // Inside first then
  // value undefined

Promise는 resolve든 reject든 결정되고 난 후에 다시는 실행되지 않습니다.

 

## reject와 resolve 둘 중 하나가 실행되고 난 이후 Promise 내부 상황

Javascript는 콜스택이 빌때까지 계속 실행되기 때문에 먼저 작성된 reject가 실행이 되더라도 reject 다음줄에 작성한 resolve도 실행될 것이라 예측해 볼 수 있겠지만 그렇지 않습니다. 

new Promise((resolve, reject) => {
  console.log('Inside promise')
  reject(new Error('First reject'))
  console.log('before resolve');
  resolve('First resolve')
  console.log('after resolve');
})
  .catch((error) => {
    console.log('error', error)
  })
  .then((value) => {
    console.log('Inside first then')
    console.log('value', value)
  })
  
  // 실행결과
  // Inside promise
  // before resolve
  // after resolve
  // Error: First reject
  // Inside first then
  // value undefined

위처럼 reject가 실행된 다음 줄부터 Promise안의 다른 console.log는 실행이 되지만 resolve는 실행되지 않습니다.

반대로 resolve와 reject를 바꿔서 실행하면, 역시 먼저 실행된 resolve만 실행되고 reject는 실행되지 않습니다.

new Promise((resolve, reject) => {
  console.log('Inside promise')
  resolve('First resolve')
  console.log('before reject');
  reject(new Error('First reject'))
  console.log('after reject');
})
  .catch((error) => {
    console.log('error', error)
  })
  .then((value) => {
    console.log('Inside first then')
    console.log('value', value)
  })
  
  // 실행결과
  // Inside promise
  // before reject
  // after reject
  // Inside first then
  // value First resolve

즉, reject든 resolve든 둘 중에 먼저 실행되어 결정된 값으로 정해지고 결정된 이후에 실행된 reject나 resolve는 실행되지 않습니다.

 

## Promise의 유용한 예

가장 예로 들기 쉬운 경우가 비동기 코드인 setTimeout입니다.

1. 비동기코드를 직렬로 연결

new Promise((resolve, reject) => {
  console.log('Before timeout')
  setTimeout(() => {
    resolve(Math.random())
    console.log('After resolve')
  }, 1000)
})
  .then((value) => {
    console.log('then 1')
    console.log('value', value)
  })
  .then(() => {
    console.log('then 2')
  })
  .then(() => {
    console.log('then 3')
  })
// 실행 결과
// Before timeout
// After resolve
// then 1
// value 0.93425245
// then 2
// then 3

위 코드에서는 처음에 setTimeout을 사용해서 비동기 처리를 하는 것은 좋은데, 그 뒤에 이어서 작성한 then은 전부 동기적으로 실행되니 큰 의미가 없지 않냐고 생각할 수도 있겠습니다.

 

2. then에서 Promise를 return하기.

then이 할 수 있는 특별한 일이 있습니다. then에서 promise를 return하면 다음 then에서 promise가 끝나기를 기다립니다.

좀 더 구체적인 예시.

function returnPromiseForTimeout() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(Math.random())
    }, 1000)
  })
}

returnPromiseForTimeout()
  .then((value) => {
    console.log('1', value)
    return returnPromiseForTimeout()
  })
  .then((value) => {
    console.log('2', value)
    return returnPromiseForTimeout()
  })
  .then((value) => {
    console.log('3', value)
    return returnPromiseForTimeout()
  })
  .then((value) => {
    console.log('4', value)
  })
  
  // 실행 결과
  // 1 0.3445543
  // 2 0.5766575
  // 3 0.8657567
  // 4 0.6453434

이런식으로 Promise chain이 가능해집니다.

만약 위 코드를 Promise 없이 작성한다면 어떤 코드가 되었을까요? callback 형태의 callback지옥 모양이 되었을 것입니다.

 

아래는 위 코드를 Promise없이 작성한 코드.

setTimeout(() => {
  const value = Math.random()
  console.log('1', value)
  setTimeout(() => {
    const value = Math.random()
    console.log('2', value)
    setTimeout(() => {
      const value = Math.random()
      console.log('3', value)
      setTimeout(() => {
        const value = Math.random()
        console.log('4', value)
      }, 1000)
    }, 1000)
  }, 1000)
}, 1000)

또는

function bounceCode(callback) {
  setTimeout(() => {
    const value = Math.random()
    callback(value)
  }, 1000)
}

bounceCode((value) => {
  console.log('1', value)
  bounceCode((value) => {
    console.log('2', value)
    bounceCode((value) => {
      console.log('3', value)
      bounceCode((value) => {
        console.log('4', value)
      })
    })
  })
})

 

## Node에서 제공하는 Promise 스타일의 API 예시

Node에서도 Promise 스타일의 API를 제공해줍니다.

예를 들어 .gitignore 파일을 읽는 코드를 작성시에 아래처럼 promise 스타일의 API를 사용할 수 있습니다.

fs.promises.readFile('.gitignore').then((value) => console.log(value));

만약 Promise 스타일의 API를 제공하지 않았다면.. 아래 처럼 작성해야 될 것입니다.

const fs = require('fs')

function readFileInPromise(fileName) {
  return new Promise((resolve, reject) => {
    fs.readFile('.gitignore', 'utf-8', (error, value) => {
      if (error) {
        reject(error)
      }
      resolve(value)
    })
  })
}

readFileInPromise('.gitignore').then((value) => console.log(value))

 

## Promise와 async, await 키워드와의 관련성

  • async함수 안에서 반환된 Promise에 await 키워드를 사용하여 Promise의 반환값을 기다릴 수 있습니다.
  • async 함수는 결국 Promise를 돌려주는 함수입니다. async 함수는 항상 promise를 반환합니다. 만약 async 함수의 반환값이 명시적으로 promise가 아니라면 암묵적으로 promise로 감싸집니다.
    예를 들어
async function foo() {
        return 1
    }

       위 코드는 아래와 같습니다.

function foo() {
        return Promise.resolve(1)
    }
  • async 함수의 본문은 0개 이상의 await 문으로 분할된 것으로 생각할 수 있습니다. 첫번째 await 문을 포함하는 최상위 코드는 동기적으로 실행됩니다. 따라서 await 문이 없는 async 함수는 동기적으로 실행됩니다. 하지만 await 문이 있다면 async 함수는 항상 비동기적으로 완료됩니다.
    예를 들어
    async function foo() {
        await 1
    }

       위 코드는 아래와 같습니다.

    function foo() {
        return Promise.resolve(1).then(() => undefined)
    }

 

### 예시 코드

function sleep(duration) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(undefined)
    }, duration)
  })
}

async function main() {
  console.log('first')
  await sleep(1000)
  console.log('second')
  await sleep(1000)
  console.log('third')
  await sleep(1000)
  console.log('finish!')
}

main()
// 실행 결과 (1초 마다 출력..)
// first
// second
// third
// finish!

 

### async await에서 에러 잡기

try catch block을 사용합니다.

const fs = require('fs');

async function main() {
    try {
        const result = await fs.promises.readFile('.gitignore', 'utf-8');
        console.log(result);
    } catch (error) {
        console.log('error', error);
    }
}

main();

 

# 모던 JavaScript - polyfill, transpile

모던 JavaScript의 기능을 100% 사용할 수 있게 해주는 polyfill, transpile 에 대해서 알아보겠습니다.

 

## polyfill

polyfill이란? 
JS standard library에 표준으로 등록되어 있으나, 아직 브라우저나 Node.js에서 구현되지 않은 기능을 미리 써 볼 수 있도록 만들어진 구현체를 뜻합니다.
core.js 등이 그 예시입니다.

경우에 따라서 이미 만들어진 제품의 버전을 올릴 수는 없는데 상위 버전의 javascript나 node의 기능을 사용하고 싶을 때, 이런 경우를 해결해 줄 수 있는 것이 core.js 같은 라이브러리를 활용하는 것입니다.

core.js의 github에서 이에 대한 정보를 확인할 수 있습니다. https://github.com/zloirock/core-js

 

core.js의 사용 예

// core-js를 require해서 사용.
require('core.js');

// 예로 flat()을 사용해보겠습니다.
const complicatedArray = [1, [2, 3]];
const flattendArray = complicatedArray.flat();

console.log(flattendArray); // [1, 2, 3]

 

### polyfill인지 아닌지 확인하는 방법

node에서 polyfill인지 아닌지 node.green에서 확인할 수 있습니다. https://node.green/

node 버전 14이상정도되면 거의 대부분은 구현이 되어 있습니다.

 

### 사용 예시

String.prototype.replaceAll를 예시로 polyfill의 역할을 테스트 해보겠습니다.

// require('core.js')

// node 버전: v10.24.1
// abc를 123으로 바꾸는 것이 목적입니다.
const original = 'abcabc123'
const changed = original.replaceAll('abc', '123')
console.log(changed); // error: replaceAll이 존재하지 않습니다.

---------------------------------------------------------------------

require('core.js')

// node 버전: v10.24.1
// abc를 123으로 바꾸는 것이 목적입니다.
const original = 'abcabc123'
const changed = original.replaceAll('abc', '123')
console.log(changed); // 123123123

 

## transpile

transpile이란?
코드를 A 언어에서 B 언어로 변환하는 작업을 뜻합니다.
자바스크립트의 경우 보통 구형 런타임(브라우저, 혹은 구버전 Node 등)에서 신규 문법적 요소(optional chaining 등)를 활용하기 위해 사용합니다.

즉, 신규 언어 스펙(ES6+)에서 구형 언어 스펙(ES5 등)으로 트랜스파일을 할 때 주로 사용됩니다.

자바스크립트를 대상으로 하는 트랜스파일러는 Babel, tsc(TypeScript Compiler), ESBuild 등이 있습니다.

esbuild 라는 툴은 매우 심플하게 예전 버전의 자바스크립트 파일을 트랜스파일을 할 수 있습니다.

 

### polyfill과 transpile

예를 들어 optional chainning 은 문법이기 때문에 위에서 말한 polyfill로 해결할 수 없습니다. polyfill은 없는 함수를 만들어 주는 용도입니다. node가 구버전이면 문법자체를 이해를 하지 못하기 때문에 이 경우에 transpile이 필요합니다.

 

 

 

댓글수0