동기(Synchronous)

동기 방식은 서버에 요청을 보냈을 때, 응답이 돌아와야 다음 동작을 수행할 수 있다.

즉, A 작업이 끝나야 B 작업이 실행된다.

 

비동기(Asynchronous)

비동기 방식은 서버에 요청을 보냈을 때, 응답 상태와 상관없이 다음 동작을 수행할 수 있다.

즉, A 작업이 진행되면서 B 작업이 실행되고, A 작업이 끝나면 B 작업과 상관없이 결괏값이 출력된다.

 

동기 / 비동기

 

스레드

스레드란, CPU가 프로그램을 동작시키는 최소 단위이다.

운영체제에서 프로그램이 실행되고 있는 상태를 프로세스라고 하고, 프로세스는 1개의 메인 스레드와 여래 개의 작업 스레드를 동작시킨다.

웹 브라우저나 NodeJs 자체는 다중 스레드로 동작하지만, 자바스크립트(타입스크립트)는 단일 스레드로 동작한다.

 

단일 스레드, 싱글 스레드

싱글 스레드는 프로세스 내에서 하나의 메인 스레드만으로 작업을 처리한다.

즉, 작업을 순서대로 처리하고, 만약 선행 스레드의 작업이 매우 길어진다면, 후행 스레드는 선행 작업이 끝날 때까지 기다려야 한다.

 

다중 스레드, 멀티 스레드

멀티 스레드는 CPU의 최대 활용을 위해 2개 이상의 스레드를 동시에 실행시켜 작업을 처리한다.

문맥 교환(context switching)을 통해서 각 스레드의 작업을 조금씩 처리하고, 사용자의 입장에서 프로그램들이 동시에 수행되는 것처럼 보인다.

 

 


07-1 비동기 콜백 함수

동기와 비동기 API

  • 동기 버전

ex) readFileSycn, ~Sync

NodeJs에서 파일 읽기는 readFileSync라는 이름의 API를 사용해서 구현하고, Buffer라는 타입으로 전달해 준다.

Buffer는 NodeJs가 제공하는 클래스로서 바이너리 데이터를 저장하는 기능을 수행한다.

 

  • 비동기 버전

ex) readFile

readFile(파일경로, 콜백함수: (error: Error, buffer: Buffer) => void)

콜백 함수의 첫 번째 매개변수를 통해서 예외 처리를 하고, 두 번째 매개변수를 콜백 함수에 전달한다.

 

동기 방식 API는 파일 내용을 모두 읽을 때(= 작업이 종료될 때)까지 프로그램의 동작을 잠시 멈춘다.

비동기 방식 API는 프로그램의 동작을 멈추지 않는 대신 결과를 콜백 함수로 얻는다. (= 비동기 콜백 함수)

import { readFileSync, readFile } from "fs";

// 동기 방식
console.log('동기 방식 API')

const buffer: Buffer = readFileSync('./text.json') // 일시적으로 멈춤
console.log(buffer.toString())


// 비동기 방식
readFile('./text.json', (error: Error, buffer: Buffer) => {
  console.log(' 비동기 방식 API')
  console.log(buffer.toString())
})


// Promise and async/await
const readFilePromise = (filename: string): Promise<string> =>
  new Promise<string>((resolve, reject) => {
    readFile(filename, (error: Error, buffer: Buffer) => {
      if(error)
        reject(error)
      else
        resolve(buffer.toString())
    })
  });

  (async () => {
    const content = await readFilePromise('./text.json')
    console.log('Promise and async/await')
    console.log(content)
  })()

 

위의 코드의 출력 화면을 보면 코드의 작성 순서대로 출력되는 것이 아니라 동기, 비동기 처리 순서에 따라 출력이 달라짐을 알 수 있다.

 

 

단일 스레드와 비동기 API

자바스크립트와 타입스크립트는 단일 스레드로 동작하므로 될 수 있으면 readFileSync와 같은 동기 API는 사용하지 말아야 한다.

동기 API는 코드를 작성하기는 쉽지만, 결괏값이 반환될 때까지 일시적으로 멈추기 때문에 웹 브라우저에서 웹 서버로 접속되지 않는 현상이 발생하고, 프로그램의 반응성을 훼손하게 된다.

따라서 자바스크립트와 타입스크립트에서 동기 API는 사용하지 말아야 한다.

 

콜백 지옥

콜백 지옥은 콜백 함수에서 또 다른 비동기 API를 호출하는 코드를 의미한다.

함수 안에 함수 호출이 반복되면 코드의 가독성이 떨어질 뿐만 아니라 코드의 유지보수 또한 어렵기 때문에 콜백 지옥이 되지 않게끔 코드를 작성해야 한다.

import { readFile } from "fs";

// 비동기
readFile('./text.json', (err: Error, buffer: Buffer) => {
  if(err) throw err
  else {
    const content: string = buffer.toString()
    console.log(content)
  }

  // 비동기
  readFile('./text1.json', (err: Error, buffer: Buffer) => {
    if(err) throw err
    else {
      const content: string = buffer.toString()
      console.log(content)
    }
  })
})

 


07-2 Promise 이해하기

자바스크립트에서 프로미스는 Promise라는 이름의 클래스이다.

Promise 클래스를 사용하려면 new 연산자를 적용해 프로미스 객체를 만들어야 한다.

 

Promise는 비동기 작업의 최종 완료 또는 실패를 나타나는 객체이고,

Promise의 콜백 함수는 resolve와 reject라는 2개의 매개변수를 갖는다.

 

  • resolve(value): 작업이 성공적으로 끝난 경우, 그 결괏값을 value와 함께 호출
  • reject(error): 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
new Promise<T>((
  resolve: (value: T) => void,
  
  reject: (error: Error) => void) => {
  // 코드 구현
  })
})

 

프로미스의 3가지 상태(states)

new Promise()로 프로미스를 생성하고 종료될 때까지 3가지의 상태를 갖는다.

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

 

Pending(대기)

new Promise() 메서드를 호출하면 대기 상태가 된다.

new Promise(function(resolve, reject){
  ...
}

 

Fulfilled(이행)

콜백 함수의 인자 resolve를 실행하면 이행 상태가 되고, 이행 결괏값을 then()을 이용해 받을 수 있다.

new Promise(function(resolve, reject){
  resolve()
}

 

Rejected(실패)

콜백 함수의 인자 reject를 실행하면 실패 상태가 되고, 실패 결괏값을 reject()를 이용해 받을 수 있다.

new Promise(function(resolve, reject){
  reject()
}

 

 

resolve와 reject 함수

Promise 타입 객체의 then, catch,  finally 메서드를 메서드 체인 형태로 사용한다.

 

  • then

Promise에서 resolve 함수를 호출한 값은 then 메서드의 콜백 함수에 전달된다.

 

  • catch

Promise에서 reject 함수를 호출한 값은 catch 메서드의 콜백 함수에 전달된다.

 

  • finally

Promise에서 finally 함수는 항상 마지막에 호출된다.

 

import {readFile} from "fs";

export const readFilePromise = (filename: string): Promise<string> =>
  new Promise<string>((
    resolve: (value: string) => void,

    reject: (error: Error) => void) => {
      readFile(filename, (err: Error, buffer: Buffer) => {
        if (err) reject(err)
        else resolve(buffer.toString())
    })
  })

readFilePromise('./text.json')
  .then((content: string) => {
    console.log(content)
    return readFilePromise('./text1.json')
  })

  .then((content: string) => {
    console.log(content)
    return readFilePromise('.')
  })

  .catch((err: Error) => console.log('error', err.message))
  .finally(() => console.log('종료'))

 

Promise.resole 메서드

Promise.resolve(값) 형태로 호출하면 해당 '값'은 then 메서드에서 얻을 수 있다.

Promise.resolve(1)
  .then(value => console.log(value)) // 1

Promise.resolve({name: 'Jack'})
  .then(value => console.log(value)) // { name: 'Jack' }

 

Promise.reject 메서드

Promise.reject(Error 타입 객체)를 호출하면 해당 'Error 타입 객체'는 catch 메서드의 콜백 함수에서 얻을 수 있다.

Promise.reject(new Error('에러 발생'))
  .catch((err: Error) => console.log(err.message)) // 에러 발생

 

then-체인

Promise의 then 메서드를 호출할 때 사용한 콜백 함수는 값을 반환할 수 있다.

then에서 반환된 값은 또 다른 then 메서드를 호출해 값을 수신할 수 있고, then 메서드는 반환된 값이 Promise 타입이면 이를 resolve 한 값을 반환한다. reject 일 경우 catch 메서드에서 거절당한 값을 얻을 수 있다.

Promise.resolve(1)
  .then((value: number) => {
    console.log('1', value) // 1
    return Promise.resolve(true)
  })

  .then((value: boolean) => {
    console.log('2', value) // true
    return [1, 2, 3]
  })

  .then((value: number[]) => {
    console.log('3', value) // [1, 2, 3]
    return {name: 'Jack'}
  })

 

Promise.all 메서드

every 메서드는 배열의 모든 아이템이 어떤 조건을 만족하면 true를 반환한다.

const isAllTrue = (values: boolean[]) => values.every(value => value == true)

console.log(isAllTrue([true, true]))  // true
console.log(isAllTrue([true, false])) // false

 

Promise.race 메서드

some 메서드는 배열의 아이템 중 하나라도 조건을 만족하면 true를 반환한다.

const isAnyTrue = (values: boolean[]) => values.some(value => value == true)

console.log(isAnyTrue([true, false]))  // true
console.log(isAnyTrue([false, false])) // false

 


07-3 async와 await 구문

2013년 마이크로소프트는 C# 5.0을 발표하면서 비동기 프로그래밍 코드를 비약적으로 간결하게 구현할 수 있는 async/await라는 구문을 제공했다.

const test = async () => {
  const value = await Promise.resolve(1)
  console.log(value)
}

test() // 1

 

await 키워드

await 키워드는 피연산자(operand)의 값을 반환해 준다.

피연산자가 Promise 객체이면 then 메서드를 호출해 얻은 값을 반환해 준다.

 

async 함수 수정자

await 키워드는 항상 async가 있는 함수 몸통에서만 사용할 수 있다.

 

결과 화면을 보면 함수의 순서대로 1, 1, hello, hello 모양이 아니라 함수가 마치 동시에 실행된 것처럼 1, hello, 1, hello 순서로 출력됨을 볼 수 있다.

export const test1 = async () => {
  let value = await 1
  console.log(value)

  value = await Promise.resolve(1)
  console.log(value)
}

export async function test2() {
  let value = await 'hello'
  console.log(value)

  value = await Promise.resolve('hello')
  console.log(value)
}

test1()
test2()

 

async 함수의 두 가지 성질

  1. 일반 함수처럼 사용할 수 있다.
  2. Promise 객체로 사용할 수 있다.

 

async 함수를 Promise 객체로 사용하면 test1() 함수 호출이 해소(resolve)된 다음에 test2() 함수를 호출한다.

export const test1 = async () => {
  let value = await 1
  console.log(value)

  value = await Promise.resolve(1)
  console.log(value)
}

export async function test2() {
  let value = await 'hello'
  console.log(value)

  value = await Promise.resolve('hello')
  console.log(value)
}

test1().then(() => test2())

 

async 함수가 반환하는 값의 의미

async 함수는 값을 반환할 수 있고, 이때 반환값은 Promise 형태로 변환되므로 then 메서드를 호출해 async 함수의 반환값을 얻어야 한다.

const asyncReturn = async() => {
  return [1, 2, 3]
}

asyncReturn().then(value => console.log(value)) // [ 1, 2, 3 ]

 

async 함수의 예외 처리

async 함수에서 예외가 발생하면 프로그램이 비정상적으로 종료된다.

비정상으로 종료하는 상황을 막으려면 함수 호출 방식이 아니라 함수가 반환하는 Promise 객체의 catch 메서드를 호출하는 형태로 코드를 작성해야 한다.

const asyncExpection = async() => {
  throw new Error('error1')
}

asyncExpection().catch(err => console.log(err.message)) // error1

const awaitReject = async() => {
  await Promise.reject(new Error('error2'))
}

awaitReject().catch(err => console.log(err.message)) // error2

 

async 함수와 Promise.all

  1. readFileAll 함수는 filenames에 담긴 배열을 map 메서드를 적용해 Promise[] 타입 객체로 전환한다.
  2. Promise.all 메서드를 사용해 단일 Promise 객체로 만든다.
  3. 만들어진 객체에 await 구문을 적용해 결괏값을 반환한다.
  4. readFileAll 함수를 Promise 객체로 취급해 then과 catch 메서드로 연결한다.

 이런식으로 코드를 작성해 예외가 발생하더라도 프로그램이 비정상적으로 종료하지 않도록 한다.

import {readFile} from "fs";

export const readFilePromise = (filename: string): Promise<string> =>
  new Promise<string>((
    resolve: (value: string) => void,

    reject: (error: Error) => void) => {
    readFile(filename, (err: Error, buffer: Buffer) => {
      if (err) reject(err)
      else resolve(buffer.toString())
    })
  })

const readFileAll = async(filenames: string[]) => {
  return await Promise.all(filenames.map(filename => readFilePromise(filename)))
}

readFileAll(['./text.json', './text1.json'])
  .then(([text, text1]: string[]) => {
    console.log('text', text)
    console.log('text1', text1)
  })

  .catch(err => console.log(err.message))

 

 

 

참고자료

Do it! 타입스크립트 프로그래밍

+ Recent posts