자바스크립트 비동기 처리 3가지 방식

JS는 싱글 스레드 프로그래밍 언어기에 멀티 작업을 하기 위해서는 비동기 처리 방식이 자주 쓰인다.

비동기 처리는 백그라운드로 동작되기에 결과가 언제 반환될지 알 수 없어, 완료되면 결과를 받아 처리하기 위해 대표적으로 Callback 함수나, Promise 객체를 사용한다. 하지만 서비스 규모가 커질 수록 코드가 복잡해짐에 따라 코드를 중첩하여 사용하다가 가독성이 떨어지고 유지보수가 어려워지는 상황이 발생한다. 이를 Callback Hell, Promise Hell이라고 부른다.

/* Callback Hell */
getData (function (x) {
  getMoreData (x, function (y) {
    getMoreData (y, function (z) {
      ...
    });
  });
});
/* Promise Hell */
fetch('https://example.com/api')
  .then(response => response.json())
  .then(data => fetch(`https://example.com/api/${data.id}`))
  .then(response => response.json())
  .then(data => fetch(`https://example.com/api/${data.id}/details`))
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

위의 코드를 보면 Callback hell과, then 핸들러의 남용은 구현하고자하는 의도의 파악과 가독성이 크게 떨어진다.

JS의 async와 await는 이런 문제들을 해결하기 위해 탄생했으며, 가독성과 유지보수성을 향상시켜준다.

async function getData() {
    const response = await fetch('https://example.com/api');
    const data = await response.json();
    const response2 = await fetch(`https://example.com/api/${data.id}`);
    const data2 = await response2.json();
    const response3 = await fetch(`https://example.com/api/${data.id}/details`);
    const data3 = await response3.json();
    console.log(data3);
}

getData();

위 코드를 보면 마치 함수의 리턴값을 변수가 받은 정의문 형식대로 되어 있어 코드가 의도하고자 하는 바를 동일 코드 레벨 라인에서 아수가 있어 편하다.

JS async/await

async/await는 ES2017에 도입된 문법으로, Promise 로직을 더 쉽고 간결하게 사용할 수 있게 해준다. async/await가 Promise를 대체하기 위한 기능이 아니라는 것이다. 내부적으로는 여전히 Promise를 사용해서 비동기를 처리하고, 단지 코드 작성 부분을 프로그래머가 유지보수학 편하게 보이는 문법만 다르게 해줄 뿐이라는 것이다.

async/ await 기본 사용법

async/await는 절차적 언어에서 작성하는 코드와 같이 사용법도 간단하고 이해하기 쉽다. function 키워드 앞에 async만 붙여주면 되고, 비동기로 처리되는 부분 앞에 await만 붙여주면 된다.

다음은 setTimeout 비동기 함수를 이용해 delay 기능을 구현한 프로미스 객체 비동기 함수를 기존 Promise.then()방식과 async/await 방식으로 똑같이 처리하지만 다르게 코드를 구현한 예제이다.

// 프로미스 객체 반환 함수
function delay(ms) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`${ms} 밀리초가 지났습니다.`);
      resolve()
    }, ms);
  });
}

// 기존 Promise.then() 형식
function main() {
  delay(1000)
      .then(() => {
        return delay(2000);
      })
      .then(() => {
        return Promise.resolve('끝');
      })
      .then(result => {
        console.log(result);
      });
}

// 메인 함수 호출
main();

// async/await 방식
async function main() {
  await delay(1000);
  await delay(2000);
  const result = await Promise.resolve('끝');
  console.log(result);
}

// 메인 함수 호출
main();

예시 코드를 보면 promise는 then 메서드를 연속적으로 사용하여 비동기 처리를 한다.
async/await는 await 키워드로 비동기 처리를 기다리고 있다는 것을 직관적으로 표현하고 있음을 볼 수 있다.

async/await의 장점을 비동기적 접근방식을 동기적으로 작성할 수 있게 해주어 코드가 간결해지며 가독성을 높여져 유지보수를 용이하게 해준다.

async 키워드

await를 사용하기 위한 선언문이다. 즉 function앞에 async를 붙여줌으로써, 함수 내에서 await 키워드를 사용할 수 있게 된다.

// 함수 선언식
async function func1() {
    const res = await fetch(url); // 요청을 기다림
    const data = await res.json(); // 응답을 JSON으로 파싱
}
func1();

// 함수 표현식
const func2 = async () => {
    const res = await fetch(url); // 요청을 기다림
    const data = await res.json(); // 응답을 JSON으로 파싱
}
func2();

async 리턴값은 Promise 객체

async fuction에서 어떤 값을 리턴하든 무조건 promise 객체로 감싸져 반환된다.

async function func1() {
  return 1;
}

const data = func1();
console.log(data); // 프로미스 객체가 반환된다

다른 Promise 상태를 반환하기

직접 Promise 정적 메서드를 통해 다음과 같이 Promise 상태(state)를 다르게 지정하여 반환이 가능하다.

async function resolveP() {
  return Promise.resolve(2);
}

async function rejectP() {
  return Promise.reject(2);
}

reject 같은 경우 위와 같이 Promise.reject() 정적 메서드를 통해 반환되는 Promise 상태를 실패(rejected) 상태로 지정해줄 수있지만, async 함수 내부에서 예외 throw를 해도 실패 상태의 Promise 객체가 반환된다.

async function errorFunc() {
    throw new Error("프로미스 reject 발생시킴");
}

만일 async fuction에서 일부러 return을 하지 않아도 자동으로 return undefined으로 처리되게 때문에 어찌됬든 무조건 Promise 객체를 반환하게 된다.

async 함수와 then 핸들러

Promise 객체를 반환하기에 then 핸들러를 붙일 수 있다. 허나 then 핸들러를 남용할 경우 Promise Hell에 걸릴 수 있기에 대신 await 사용을 권장한다.

async function func1() {
  return 1;
}

func1()
    .then(data => console.log(data));

await 키워드

await 키워드는 promise.then() 보다 좀 더 세련되게 비동기 처리의 결과값을 얻을 수 있도록 해주는 문법이다.

예를 들어, 서버에 리소르를 요청하는 fetch() 비동기 함수를 다음과 같이 then 핸들러 방식으로 결과를 얻어 사용해왔을 것이다.

// then 핸들러 방식
fetch(url)
    .then(res => res.json()) // 응답을 JSON으로 파싱
    .then(data => {
      // data 처리
      console.log(data);
    })

await 키워드를 사용하면 then 핸들러를 복잡하게 처리할 필요 없이, 심플하게 비동기 함수 왼쪽에 await만 명시해주고 결과값을 변수에 받도록 코드를 정의하면 끝이다. then과 콜백 함수를 남발하여 코드가 들여쓰기로 깊어지는 것을 방지하고, 한 줄 레벨에서 코드를 나열하여 가독성을 높일 수 있다.

// await 방식
async function func() {
    const res = await fetch(url); // 요청을 기다림
    const data = await res.json(); // 응답을 JSON으로 파싱
    // data 처리
    console.log(data);
}
func();

await는 Promise 처리가 끝날때까지 기다림

await는 Promise 비동기 처리가 완료될때 까지 코드 실행을 일시 중지하고 wait한다. 예를 들어 fetch() 함수를 사용하여 서버에서 데이터를 가져오는 경우를 생각해보자. 이 함수는 Promise를 반환한다. 따라서 await 키워드를 사용하여 이 Promise가 처리될 때까지 코드 실행을 일시 중지하고, Promise가 처리되면 결과 값을 반환하여 변수에 할당하는 식이다.

async function getData() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
  const data = await response.json();
  console.log(data):
}

await는 Promise를 처리하고 결과를 반환하는 데, 비동기적인 작업을 동기적으로 처리할 수 있게 해준다.

async/await 에러 처리

기존의 Promise.then() 방식의 에러 처리는 catch() 핸들러를 중간 중간에 명시함으로써 에러를 받아야 했다.

// then 핸들러 방식
function fetchResource(url) {
  fetch(url)
    .then(res => res.json()) // 응답을 JSON으로 파싱
    .then(data => {
      // data 처리
      console.log(data);
    })
    .catch(err => {
      // 에러 처리
      console.error(err);
    });
}

async/await의 경우 try/catch를 통해서 에러를 처리한다.

// async/await 방식
async function func() {

    try {
        const res = await fetch(url); // 요청을 기다림
        const data = await res.json(); // 응답을 JSON으로 파싱
        // data 처리
        console.log(data);
    } catch (err) {
        // 에러 처리
        console.error(err);
    }

}
func();

async/await 함정과 병렬처리

비동기 프로그래밍은 웹에서 메인 스레드를 차단하지 않고 시간 소모적인 작업을 병렬적으로 수행할 수있도록 하는 웹 개발의 필수적인 부분이다.
JS에서 비동기 코드를 처리하는 문법으로 Promise, await 키워드를 사용하는 것 이다. 그러나 await를 남발하면 성능 문제 및 기타 문제가 발생할 수 있다. await는 Promise가 해결될 때까지 함수 실행을 일시 중지 하는 것인데, 병렬적으로 멀티로 처리할 수 있는 작업을 억지로 동기적으로 처리하게 함으로써 오히려 2초만에 해결할 로직을 6초씩이나 걸리게 할 수 있기 때문이다.

따라서 await를 올바르게 사용하지 않으면 오히러 성능 문제가 발생하기에, JS await 남용에 대한 몇 가지 주의 사항과 Promise.all을 통해 병렬 처리를 최적화하는 방법을 살펴볼 예정이다.

적절하지 않은 async/await 사용

function getApple(){
  return new Promise( (resolve, reject) => {
    setTimeout(() => resolve("apple"), 1000);
  })
}

function getBanana(){
  return new Promise( (resolve, reject) => {
    setTimeout(() => resolve("banana"), 1000);
  })
}

async function getFruites(){
  let a = await getApple(); 
  let b = await getBanana(); 
  console.log(`${a} and ${b}`);
}

getFruites();

위의 코드에서 apple과 banana는 1초가 걸린다. 만일 이를 병렬적으로 처리하면 console에 찍히기 까지 1초가걸리지만, 동기적으로 처리하면 2초가 걸린다. 위의 비동기함수 getApple, getBanana는 서로 연관이 없다. 그럼에서 생각없이 await 키워드를 두 번 붙이면 동기적으로 실행되는데, 이는 명백한 시간 낭비이다.

async function getFruites(){
  console.time();
  let a = await getApple(); // 1초 소요
  let b = await getBanana(); // 1초 소요
  console.log(`${a} and ${b}`);
  console.timeEnd();
}

getFruites();

적절한 async/await 사용

핵심은 Promise 객체 함수를 await와 같이 써서 실행시키는 것이 아니라, 미리 함수를 동기/논블록킹으로 실행하고 그 결과 Promise값을 await를 통해 받는 식이다.

위의 코드 처럼 getApple(), getBanana() 비동기 로직이 만일 순서를 지켜야하는 로직이라면 위와 같이 구성하는게 옳지만, 현재느 서로 연관이 없기에 반드시 순차적으로 실행 시킬 필요가 없다. 따라서 비동기 처리 요청과 값을 await하는 로직을 분리시키면 된다.

async function getFruites(){

  // 미리 실행시킴
  let getApplePromise = getApple(); // async함수를 미리 논블록킹으로 실행한다. 
  let getBananaPromise = getBanana(); // async함수를 미리 논블록킹으로 실행한다. 

  // 이렇게 하면 각각 백단에서 독립적으로 거의 동시에 실행되게 된다.
  console.log(getApplePromise)
  console.log(getBananaPromise)

  let a = await getApplePromise; // 위에서 받은 프로미스객체 결과 변수를 await을 통해 꺼낸다.
  let b = await getBananaPromise; // 위에서 받은 프로미스객체 결과 변수를 await을 통해 꺼낸다.

  console.log(`${a} and ${b}`); // 본래라면 1초+1초 를 기다려야 하는데, 위에서 1초기다리는 함수를 바로 연속으로 비동기로 불려왔기 때문에, 대충 1.01초만 기다리면 처리된다.
})

Promise.all

또 다른 방법으로는 Promise.all() 정적 메서드를 사용하는 방법이 있다. 위와 같이 구성할 경우 비동기 처리 완료 시점을 가늠하기 힘들기에 대부분 Promise.all()로 처리한다

Promise.all()은 배열 인자의 각 Promise 비동기 함수들이 모두 resolve되어야 결과를 리턴 받는다. 배열 인자의 각 promise 함수들은 제각각 비동기 논블로킹으로 실행되어 시간을 단축할 수 있다. return 값은 각 promise 함수의 반환값들이 배열로 담겨져 있다.

async function getFruites(){
  console.time();

  // 구조 분해로 각 프로미스 리턴값들을 변수에 담는다.
  let [ a, b ] = await Promise.all([getApple(), getBanana()]); 
  console.log(`${a} and ${b}`);

  console.timeEnd();
}

getFruites();

출처

https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%B2%98%EB%A6%AC-async-await

'JS' 카테고리의 다른 글

동기, 비동기  (1) 2024.06.12
Call back 함수  (1) 2024.06.11
Binding의 개념과 call, apply, bind  (1) 2024.06.08

자바스크립트의 동기와 비동기

JS는 싱글 스레드 언어이다. 때문에 한 번에 하나의 작업만 수행할 수 있다. 즉, 이전 작업이 완료되어야 다음 작업을 수행할 수 있다.
일반적으로 함수와 코드들이 위에서 아래로 차례로 동작하는 방식이라고 할 수 있다. 이러한 코드 순차 실행을 동기(Synchronous)라고 한다.

이러한 동기 방식은 간단하고 직관적이지만, 작업이 오래 걸리거나 응답이 늦어지는 경우에는 전체적인 성능과 사용자 경험에 영향을 줄 수 있다. 예를 들어, 서버에 데이터를 요청하고 응답을 받아야 하는 작업이 있다면, 응답이 올 때까지 다른 작업을 하지 못하고 대기해야 한다. 이렇게 되면 프로그램의 흐름이 멈추거나 지연되게 된다.

따라서 JS에서 여러 작업을 동시에 처리하기 위해 비동기(Asynchronous) 라는 개념을 도입하여, 특정 작업의 완료를 기다리지 않고 다른 작업을 동시에 수행할 수 있도록 하였다. 대표적으로 setTimeout(), fetch() 함수가 있다. 비동기는 메인 스레드가 작업을 다른 곳에 인가하여 처리되게 하고, 그 작업이 완료되면 콜백 함수를 받아 실행하는 방식이다. 즉, 작업을 백그라운드에 요청하여 처리되게 하여 멀티로 작업을 동시에 처리하는 것으로 볼 수 있다.

서버에 데이터를 요청하고 응답을 받아야 하는 작업이 있다면, 응답이 오는 것과 상관없이 다른 작업을 계속 이어나가 병렬로 작업을 동시 처리가 가능해져 프로그램의 흐름이 멈추거나 지연되지 않게 된다. 따라서 task들이 병렬적으로 동시에 처리되게 되고 총 코드 실행 시간은 획기적으로 줄어들게 된다.

비동기 처리의 유용성

예를 들어 DB에 접근해야 한다고 가정하자. 이를 동기적으로 수행한다면 DB의 응답이 올 때 까지 기다려야한다. 이 때 웹 애플리케이션은 다른 요청을 수행하지 못한다. 만일 대규모 트래픽이 발생할 경우 성능이 크게 저하될 것 이다.

대표적으로 웹에서 비동기 처리를 가능케 하는 Ajax 기술이 있다. 다른 서버에게 데이터를 요청할 때 XMLHttpRequest 객체나 혹은 fetch 메서드로 요청을 하게 된다. 서버로부터 응답을 기다리는 동안에도 사용자와의 interaction을 유지할 수 있으므로 사용자 경험을 향상시킬 수 있게 된다.

// fetch 함수에 URL 전달
fetch("https://jsonplaceholder.typicode.com/todos/1")
  .then(function(response) {
    return response.json(); // 응답을 JSON 형식으로 변환
  })
  .then(function(data) {
    console.log(data); // JSON 데이터를 출력
  })
  .catch(function(error) {
    console.error(error); // 에러를 출력
  });

비동기의 병렬 처리 원리

블로킹, 논블로킹

JS는 싱글 스레드이다. 블로킹을 하게되면 특정 작업이 끝나기 전까지 다음 작업으로 넘어갈 수 없다. 그래서 JS는 논블로킹으로 작업을 수행하게 된다.

논블로킹이 동작하는 방법과 그 구조

(아래의 JS는 "JS엔진"을 의미한다.)

JS 엔진

JS 파일을 그대로 사용하게 되면 해당 파일은 사람이 작성한 것이기 때문에 컴퓨터는 알지 못한다.
그렇기에 이를 기계어로 변역해줘야 하는데 이를 JS엔진이 담당한다.
JS엔진은 Memory Heap과 Call Stack을 사용해서 우리가 작성한 JS파일을 기계어로 변환하여 컴퓨터가 실행할 수 있도록 한다.

Memory Heap, Call Stack

  • Memory Heap: JS 엔진은 이 공간을 사용하여 프로그램 실행 중 필요한 메모리를 할당하고 해제한다. garbage collection을 이용하여 사용되지 않는 메모리를 주기적으로 정리하여 메모리 누수를 방지한다.
  • Call Stack: Call이 Stack처럼 쌓이는 Stack이다. 여기서 Call은 Function을 호출하는 것을 의미한다. Call Stack을 통해서 어떤 함수가 실행하고 있는지 혹은 JS파일의 어떤 부분이 실행되고 있는 지를 알 수 있다.

브라우저 상에서의 API 사용

JS 파일이 실행되는 환경은 다양하다. Node.js는 OS에서 실행 될 수도 있고 브라우저에서도 실행될 수 있다. 위의 이미지 경우 브라우저에 해당하는데 브라우저 상에서 실행된다는 것은 브라우저가 제공하는 API들이 있고 API를 사용할 수 있다는 뜻이다. API에는 종류가 많으나 대표적으로 DOM(document object module), AJAX(Asynchronous JavaScript and XML), Timeout(setTImeout) 이 있다.

Event Loop, Callback Queue

Event Loop는 JS의 Event driven이라는 말이 있다. Event 단위로 실행 되어진다는 의미로 해석하면 된다.


Callback QueueCallback이 Queue 처럼 쌓인다는 뜻이다. Queue도 마찬가지로 Data Structure의 한 종류이다. Callback은 함수로, 다른 함수에게 인자로 전달되어진 함수이다.


논블로킹에서 작업1이 실행되는 동안 작업2가 실행되는데 실행되는 곳이 Web API이다. 그리고 Web API에서 실행되고 난 것을 Callback queue로 받아주고 Event Loop가 Call Stack으로 던져준다.

  1. 작업1 실행: 메인 스레드에서 작업1이 실행된다.
  2. 작업2 실행:
    • 작업1이 실행되는 동안 작업 2도 실행된다.
    • 작업2는 브라우저의 Web API(e.g. setTimeout, XHR, fetch 등)에서 처리된다.
  3. Callback Queue:
    • Web API에서 실행이 완료되면 결과를 Callback Queue로 전달한다.
  4. Event Loop:
    • Event Loop는 Call Stack이 비어 있는지 확인하고, 비어 있다면 Callback Queue에서 작업을 가져와 Call Stack으로 던진다.

console.log("Hi");
setTImeout(fucntion cb1(){
    console.log('cb1');
}, 5000);
console.log("Bye");
  1. state, browser console, call stack 전부 clear하다.

  1. console.log('Hi')가 Call Stack에 추가된다.

  1. console.log("Hi")가 browser console에 출력된다.

  1. console.log('Hi')가 Call Stack에서 제거된다.

  1. setTimeout(function cb1(){...}) 도 함수이기에 Call Stack에 추가된다.

  1. setTimeout 함수는 실행이 되면 Web API를 호출하게 되는데 Web API 중 Timer API를 호출한다. setTimeout에 두 번째로 넘겨준 인자 5000ms 만큼을 Timer로 재서 그 시간이 지나고 나서야 첫 번째로 넘겨준 함수인 console.log('cb1')을 실행하게 된다.

  1. setTimeout(function cb1(){...}) 이 자체는 완료되어 Call Stack에서 제거된다.

  1. console.log("Bye")는 Call Stack에 추가된다. 콜스택이 비어있기에 다음 함수인 console.log('Bye')가 들어오게 된다. 콜스택이 비어있다면 입력 했던 코드 한줄 한줄 순서대로 들어오기 때문이다. JS는 콜스택을 비우고 싶어하기에 내부의 console.log('Bye')를 실행시켜준다.

  1. console.log('Bye')가 실행된다.

  1. console.log("Bye")가 Call Stack에서 제거된다.

  1. Timer의 5000ms가 지나면 콜백함수 cb1이 Callback Queue 안으로 들어오게 된다.

  1. Event Loop 가 Callback Queue의 cb1을 가지고 이를 Call Stack으로 넣어준다.

  1. cb1이 실행되고 console.log('cb1')이 Call Stack에 추가된다. cb1이 종료되기 전에 console.log('cb1')이 호출되었기에 위에 쌓인다.

  1. console.log('cb1')이 실행된다.

  1. console.log('cb1')이 call stack에서 제거된다.

  1. cb1은 call stack에서 제거된다.

위는 비동기 병렬 처리의 원리이다. 비동기 함수의 콜백 함수가 이벤트 루프에 의해서 Callback Queue에 담기고 다시 싱글 스레드인 Call Stack에 담겨서 콜백 함수가 실행된다.

자바스크립트는 싱글 스레드 언어인데 어떻게 작업(task)들은 동시에 처리가 가능하는 것인가?


자바스크립트를 실행하는 콜 스택(Call Stack)은 싱글 스레드지만, 서버에게 리소스를 요청하거나 파일 입출력 혹은 타이머 대기 작업을 실행하는 Web APIs 들은 멀티 스레드이기에 동시 작업 처리가 가능하기 때문이다. ( 간단히 백그라운드에서 동시에 처리된다고 이해하면 된다.)

Web API는 타이머, 네트워크 요청. 파일 입출력, 이벤트 처리 등 브라우저에서 제공하는 다양한 API를 포괄하는 API의 총칭이다. 브라우저마다 다르겠지만, 크롬 브라우저일 경우 Web API는 멀티 스레드로 구현되어 있다.

브라우저라는 소프트웨어가 멀티 스레드이기 때문에 메인 자바스크립트 스레드를 차단하지 않고 다른 스레드를 사용하여 Web API의 작업을 처리하여 동시 처리가 가능한 것이다.


만일 아래와 같이 3초를 대기하는 setTimer 비동기 함수와 그 외 작업(Task)들이 있다고 한다면, 이 setTimeout 코드가 Web APIs들 중 타이머 처리를 담당하는 Timer API에 넘어가서 3000ms를 병렬로 처리되면서, 동시에 메인 콜 스택의 Task1, Task2 ...를 처리하는 것이다.

setTimeout(() => {
    console.log('5초 대기 완료')
}, 3000);

Task1();
Task2();
Task3();

정리하면 브라우저는 멀티스레드로 이루어져 있고 비동기 함수는 이러한 동시적 처리 작업 원리 덕분에 우리는 비동기 함수를 통해 성능 향상을 누릴 수 있었던 것이었다.

왜 완벽한 멀티 스레딩이 아닌가

setTimeout을 이용해 비동기의 멀티 작업 처리를 설명했으나, 사실 JS의 비동기는 완벽한 멀티 스레딩이 아니다. 왜냐하면 타이머 3000ms만 병렬적으로 처리되고, 그 안의 콜백 함수 실행 코드는 추후에 이벤트 루프에 인해 CallStack에 들어가 싱글 스레드로 처리되기 때문이다.


fetch 비동기 함수도 마찬가지다. 서버에 요청해서 리소스를 다운로드하는 것은 멀티 스레드로 병렬적으로 처리되지만, 요청이 완료되고 나서의 후처리 then 핸들러의 콜백 함수는 Call Stack에 별도로 처리된다.


병렬로 동시 처리하면 되는데 왜 이런식으로 번거롭게 나눈 것인가?

완전한 병렬 처리는 성능 만큼은 이득을 얻을 수 있을지도 모르겠지만, 항상 동시성 문제가 따라와 synchronized 처리가 수반된다. 이러한 synchronized 처리를 잘못하면 오히러 성능 감소가 일어나기에 높은 지식과 실력을 요구한다. 따라서 JS는 이러한 문제를 간단히 처리하기 위해 비동기 콜백 함수 방식을 채택하였다고 보면된다.

브라우저의 비동기 처리

이러한 비동기 원리는 꼭 JS 언어 뿐만 아니라 브라우저의 HTML 마크업 언어에서도 동일하게 적용된다. HTML의 파싱 동작 방식을 예로 들 수 있다. 아래에서 <script> 태그를 HTML 파일의 <head> 태그 안에 넣으면. JS 파일이 다운로드되고 실행될 때까지 HTML 파싱이 중단되게 된다. 이는 사용자가 웹 페이지의 내용을 보는데 오래 기다리게 한다.


<!DOCTYPE html>
<html>
<head>
  <script src="script1.js"></script>
  <script src="script2.js"></script>
  <script src="script3.js"></script>
</head>
<body>
  <h1>웹 페이지 제목</h1>
  <p>웹 페이지 내용</p>
</body>
</html>

이 때 async, defer 키워드를 통해 해결할 수 있다. 이는 JS 파일을 비동기적으로 다운로드하고 실행할 수 있게 해준다. 이렇게하면 HTML 파싱과 JS 다운로드가 동시에 진행되어 페이지 로딩 속도를 향상시킬 수 있게 된다.

<!DOCTYPE html>
<html>
<head>
  <script async src="script1.js"></script>
  <script async src="script2.js"></script>
  <script async src="script3.js"></script>
</head>
<body>
  <h1>웹 페이지 제목</h1>
  <p>웹 페이지 내용</p>
</body>
</html>

비동기 처리의 문제점

다만 성능 향상을 위해 비동기 처리를 이용할 때 주의해야 할 점이 있다. Asynchronous는 요청한 작업의 완료 여부를 기다리지 않고 자신의 그 다음 작업을 계속 수행해 나간다고 했다. 그런데 만일 그 다음 실행할 작업이 이전에 요청한 작업의 결과가 반드시 필요할 경우에 문제가 생긴다.

<script async src='https://code.jquery.com/jquery-3.6.0.min.js'></script>
<script>
    // 제이쿼리 파일을 비동기적으로 호출하고 바로 제이쿼리 전용 코드를 실행 시켰기 때문에 에러가 발생하다
    $('body').append('<h1>Hello World</h1>'); // ! ERROR
</script>

아래의 코드에서 value의 값은 NaN이라는 값이 들어있다.
이는 data가 아직 반환되지 않은 상태에서 그 값을 사용하려 했기 때문이다.

function getDB() {
    let data;
    // 데이터베이스에서 값을 가져오는 3초 걸린다고 가정 (비동기 처리)
    setTimeout(() => {
        data = 100;
    }, 3000);

    return data;
}

function main() {
    let value = getDB();
    value *= 2;
    console.log('value의 값 : ', value);
}
main(); // 메인 스레드 실행

작업의 순서를 맞추는 것이 필수 불가결일 경우 어쩔수 없이 비동기를 포기하고 동기로 처리해야 되나 싶지만, 이를 해결하는 몇 가지 기법이 있다. 가장 대표적인 것이 콜백 함수 기법이다.



비동기를 알맞게 처리하기 위한 기법

비동기와 콜백 함수

콜백 함수는 JS의 특성을 이용해 함수의 매개변수에 함수 자체를 넘겨, 함수 내에서 매개변수 함수를 실행하는 기법이다.


비동기 방식은 요청과 응답의 순서를 보장하지 않는다. 따라서 응답의 처리 결과에 의존하는 경우 콜백 함수를 이용하여 작업 순서를 간접적으로 끼워 맞출 수 있다. 콜백 함수 방식으로 위의 코드를 수정하면 다음과 같다.

function getDB(callback) {
    // 데이터베이스로부터 3초 후에 데이터 값을 받아온 후, 콜백 함수 호출
    setTimeout(() => {
        const value = 100;
        callback(value);
    }, 3000);
}

function main() {
    // 호출할 작업에 콜백 함수를 넘긴다
    getDB(function(value) {
        let data = value * 2;
        console.log('data의 값 : ', data);
    });
}
main();

위의 코드는 콜백 함수를 setTimeout() 내에서 호출한다. 즉, 비동기 작업이 완료된 후에 출력이 되는 것이다. 이렇게 함으로써 작업 순서를 맞출 수 있게 되는 것이다.
따라서 비동기 함수와 콜백 함수는 서로 밀접한 관계를 가지고 있다고 말하는 것 이다.


허나 이는 코드 복잡도를 증가시켜, 개발자가 코드의 흐름을 읽기 어려워지는 등의 문제가 있을 수 있어 잘못하면 콜백 지옥에 빠질수 있다는 단점이 있다.



비동기와 프로미스 객체

콜백 함수는 비동기를 순차적으로 처리하기 위한 일종의 '편법' 같은 것이지 정식으로 지원하는 비동기 전용 함수가 아니다.
이러한 한계점을 극복하기 위해 비동기 처리를 위한 전용 객체로 Promise 객체가 등장했다.
Promise 비동기 작업의 성공 또는 실패와 그 결과값을 나타내는 객체이다.

function getDB() {
  return new Promise((resolve) => {
    setTimeout(() => {
      const value = 100;
      resolve(value);
    }, 3000);
  });
}

function main() {
  getDB()
    .then((value) => {
      let data = value * 2;
      console.log('data의 값 : ', data);
    })
    .catch((error) => {
      console.error(error);
    });
}

main();

비동기와 async/ await

하지만 Promise도 완벽한 해결책은 아니다. Callback Hell이 있듯이 지나친 then 핸들러 함수의 남용으로 인한 Promise Hell이 존재하기 때문이다. 즉, 프로미스가 여러 개 연결되면 코드가 길어지고 복잡해 질 수 있다는 것이다. 그래서 JS는 async/await라는 문법이 또한 추가되었다. 이또한 Promise를 기반으로 하지만, 동기 코드처럼 작성할 수 있게 해준다. 비동기 작업을 쉽게 읽고 이해할 수 있게 해주기에 비동기 작업을 처리할 일이 있다면 대게 async/await 방식을 쓰는 것이 보통이다.


자바 스크립트에서 Promise, async/await와 같은 문법을 사용하는 이유도 이런 비동기 처리의 흐름을 좀 더 명시적으로 하고자 하는노력이다.

function getDB() {
    return new Promise((resolve, reject) => {
        // 데이터베이스에서 값을 가져오는 3초 걸린다고 가정 (비동기 처리)
        setTimeout(() => {
            const value = 100;
            resolve(value); // Promise 객체 반환
        }, 3000);
    });
}

async function main() {
    let data = await getDB(); // await 키워드로 Promise가 완료될 때까지 기다린다
    data *= 2;
    console.log('data의 값 : ', data);
}
main(); // 메인 스레드 실행


무조건 await가 답인가?

async/ await가 비동기를 처리함에 있어 callback이나 Promise 방식보다 훨씬 좋아 보이지만, 사용 방법에 따라서는 코드가 복잡해질 수도 있다. 따라서 이 비동기 처리에 대한 3가지 방식은 용도에 맞춰서 적절히 사용해야 한다.


왜냐하면 callback 방식은 별 다른 키워드 없이도 정말 단순하게 구현할 수 있기에, 콜백 지옥을 맞이할 정도의 복잡한 상황이 아니라면 오히려 사용하면 가독성이 좋다. 대표적인 예로 Node.js의 Express 프레임워크는 서버 라우팅을 콜백 함수로 처리하는 방식을 제공한다.

const express = require("express"); // Express 모듈 불러오기
const app = express(); // Express 앱 객체 생성

// /home url 경로에 GET 요청이 들어오면 이에 대한 라우팅 정의
app.get("/home", function (req, res) {
  // 응답 보내기
  res.send("Hello, Express!");
});

즉, 간단한 비동기 작업의 처리는 콜백 함수, 그 외의 복잡한 것들은 Promise 객체를 사용하면 코드가 간결해 진다.

출처

https://inpa.tistory.com/entry/%F0%9F%8C%90-js-async
https://velog.io/@sugyinbrs/Event-Loop-Call-Stack-%EC%9D%B4-%EC%9E%91%EB%8F%99%ED%95%98%EB%8A%94-%EB%B2%95

'JS' 카테고리의 다른 글

Async/Await  (0) 2024.06.12
Call back 함수  (1) 2024.06.11
Binding의 개념과 call, apply, bind  (1) 2024.06.08

JS CallBack 함수란?

매개변수로 함수 객체를 전달해서 호출 함수 내에서 매개변수 함수를 실행하는 것을 말한다.

JS에서는 함수는 object이다. 그래서, 함수는 다른 함수의 인자로 쓰일 수도 어떤 함수에 의해 리턴될 수도 있다.
이런 함수를 '고차 함수'라고 한다.
결국, 
1. 인자로 넘겨지는 함수, 그리고 
2. 함수를 등록하기만 하고 어떤 이벤트가 발생했거나 특정 시점에 도달했을 때 시스템에서 호출하는 함수
를 콜백 함수라고 한다.

아래의 코드에서 callback 함수의 호출 제어권은 'sayHello()'에 있다.

function sayHello(name, callback) {
    const words = '안녕하세요 내 이름은 ' + name + ' 입니다.';

    callback(words); // 매개변수의 함수(콜백 함수) 호출
}

sayHello("인파", function printing(name) {
    console.log(name); // 안녕하세요 내 이름은 인파 입니다.
});

콜백 함수란 parameter로 일반적인 변수나 값을 전달하는 것이 아닌 함수 자체를 전달하는 것을 말한다.
어처피 paramter로 함수를 전달해 일회용으로 사용하기에 굳이 함수의 이름을 명시할 필요가 없다. 그렇기에 보통 익명 함수 형태로 넣어준다.

<참고>
| 단어 | 번역 | 의미 |
|---|---|---|
| parameter | 매개변수 | 함수와 메서드 입력 변수명 |
| Argument | 전달인자, 인자 | 함수와 메서드의 입력값 |

function sayHello(name, callback) {
    const words = '안녕하세요 내 이름은 ' + name + ' 입니다.';

    callback(words);
}

sayHello("인파", function (name) { // 함수의 이름이 없는 익명 함수
    console.log(name); 
});



콜백 함수 사용 원칙

  1. 익명의 함수 사용

위에서 언급한 것 처럼 콜백함수는 일회성으로 사용하는 경우가 많아, 코드의 간결성을 위해 이름이 없는 '익명의 함수'를 사용한다.

sayHello("인파", function (name) { // 함수의 이름이 없는 익명 함수
    console.log(name); 
});

또 다른 이유로는 함수 이름의 충돌 방지를 위함이다.
콜백함수에 이름을 붙이면, 그 이름은 함수 스코프 내에서 유효한 식별자가 된다. 만약 같은 스코프 내에 이미 같은 이름의 식별자가 있다면, 콜백함수의 이름이 기존의 식별자를 덮어쓰게 되어 버린다.

let add = 10; // 변수 add

function sum(x, y, callback) {
  callback(x + y); // 콜백함수 호출
}

// 이름 있는 콜백함수 작성
sum(1, 2, function add(result) {
  console.log(result); // 3
});

// 변수 add가 함수 add가 되어버린다.
console.log(add); // function add(result) {...}

 

  1. 화살표 함수 모양의 콜백

콜백 함수를 익명 함수로 정의함으로써 코드의 간결성을 얻을 수 있었지만, 화살표 함수를 통해 '익명 화살표 함수' 형태로 정의해 사용할 수 있다.

function sayHello(callback) {
  var name = "Alice";
  callback(name); // 콜백 함수 호출
}

// 익명 화살표 콜백 함수
sayHello((name) => {
  console.log("Hello, " + name);
}); // Hello, Alice

 

  1. 함수의 이름을 넘기기

javascript는 일급 객체의 특성을 가지고 있기에, JS는 null과 undefined 타입을 제외하고 모든 것을 객체로 다룬다.
그래서 매개변수에 일반적인 변수나 상수값 뿐만 아니라 함수 자체를 객체로서 전달이 가능한 것이다.
만일 콜백 함수가 일회용이 아닌 여러 호출 함수에 재활용으로 자주 이용될 경우, 별도로 함수를 정의하고 함수의 이름만 호출 함수의 인자에 전달하는 식으로 사용이 가능하다.

// 콜백 함수를 별도의 함수로 정의
function greet(name) {
  console.log("Hello, " + name);
}

function sayHello(callback) {
  var name = "Alice";
  callback(name); // 콜백 함수 호출
}

function sayHello2(callback) {
  var name = "Inpa";
  callback(name); // 콜백 함수 호출
}

// 콜백 함수의 이름만 인자로 전달
sayHello(greet); // Hello, Alice
sayHello2(greet); // Hello, Inpa



이러한 특징을 응용하면, 매개변수에 전달할 콜백 함수 종류만을 바꿔줌으로서 여러가지 함수 형태를 다양하게 전달이 가능하다.

function introduce (lastName, firstName, callback) {
    var fullName = lastName + firstName;

    callback(fullName);
}

function say_hello (name) {
    console.log("안녕하세요 제 이름은 " + name + "입니다");
}

function say_bye (name) {
    console.log("지금까지 " + name + "이었습니다. 안녕히계세요");
}

introduce("홍", "길동", say_hello);
// 결과 -> 안녕하세요 제 이름은 홍길동입니다

introduce("홍", "길동", say_bye);
// 결과 -> 지금까지 홍길동이었습니다. 안녕히계세요



콜백 함수의 활용 사례

이벤트 리스너로 사용

addEventListener는 특정 이벤트가 발생했을 때 콜백 함수를 실행하는 메서드이다. 클릭과 같은 이벤트를 처리하기 위해 등록하는 이벤트 리스너로 콜백함수가 쓰인다. 버튼을 클릭하면 그에 연관되는 스크립트 실행을 콜백 함수로 등록하는 형태인 것이다.

let button = document.getElementById("button"); // 버튼 요소를 선택

// 버튼에 클릭 이벤트 리스너를 추가
button.addEventListener("click", function () { // 콜백 함수
  console.log("Button clicked!"); 
});



고차함수에 사용

JS에서 for문 보다 더 자주 사용되는 반복문이 forEach 메서드일 것이다. 이 역시 forEach 메서드의 입력값을 콜백 함수를 전달하는 형태임을 볼 수 있다.

// 예시 : 배열의 각 요소를 두 배로 곱해서 새로운 배열을 생성하는 콜백 함수 
let numbers = [1, 2, 3, 4, 5]; // 배열 선언 
let doubled = []; // 빈 배열 선언 

// numbers 배열의 각 요소에 대해 콜백 함수 실행 
numbers.forEach(function (num) { 
    doubled.push(num * 2); // 콜백 함수로 각 요소를 두 배로 곱해서 doubled 배열에 추가 
}); 

console.log(doubled); // [2, 4, 6, 8, 10]



Ajax 결과값을 받을 때 사용

서버와 데이터를 주고받을 때 사용하는 fetch 메서드의 서버 요청의 결과값을 처리하기 위해 콜백 함수가 사용된다.

// fetch 메서드를 사용하여 서버로부터 JSON 데이터를 받아오고 콜백 함수로 화면에 출력
fetch("https://jsonplaceholder.typicode.com/users")
  .then(function (response) {
    // fetch 메서드가 성공하면 콜백 함수로 response 인자를 받음
    return response.json(); // response 객체의 json 메서드를 호출하여 JSON 데이터를 반환
  })
  .then(function (data) {
    // json 메서드가 성공하면 콜백 함수로 data 인자를 받음
    console.log(data);
  })



타이머 실행 함수로 사용

setTimeout이나 setInerval과 같은 타이머 함수에서 일정 시간마다 스크립트를 실행하는 용도로서 콜백 함수를 이용

// 3000 밀리초(3초) 후에 콜백 함수 실행
setTimeout(function () {
  console.log("Time is up!"); // 콜백 함수로 콘솔에 메시지 출력
}, 3000);

 

자바스크립트 콜백 함수 주의점

this를 사용한 콜백함수

콜백 함수 내에서 this 키워드를 사용하면, 기대한 대로 작동하지 않을 수가 있다.

let userData = {
    signUp: '2021-4-06 12:00:00',
    name: 'Not Set',
    setName: function(firstName, lastName) {
        this.name = firstName + ' ' + lastName;
    }
}

function getUserName(firstName, lastName, callback) {
    callback(firstName, lastName);
}

getUserName('홍', '길동', userData.setName);

console.log('1: ', userData.name); // Not Set
console.log('2: ', window.name); // 홍 길동

위의 userData 객체의 setName 프로퍼티 함수 내부에서 사용된
this.name이 userData 객체의 name을 가리키는게 아니라 전역 객체 window의 name을 가리키기 때문이다.



콜백 함수 this가 전역 객체인 이유

콜백 함수는 다른 함수의 인자로 전달되는 함수다. 그래서 콜백 함수는 자신을 전달받은 함수에 의해 호출되는데, 이 때 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역 객체를 참조하게 된다.

 

아래의 코드에서 userData.setName('홍', '길동')와 같이 직접 객체의 메서드 호출 방식으로 실행되었다.
메서드 호출 방식에서는 this는 함수가 들어있는 자기 자신 객체인 userData를 가리킨다. 따라서 정상적으로 userdata.name 프로퍼티 값이 업데이트 되게 된다.

let userData = {
    signUp: '2021-4-06 12:00:00',
    name: 'Not Set',
    setName: function(firstName, lastName) {
        this.name = firstName + ' ' + lastName;
    }
}

userData.setName('홍', '길동');

console.log(userData.name); // 홍 길동

 

반면에 아래와 같은 경우에는 getUserName('홍', '길동', userData.setName)와 같이 userData.setName 메서드가 콜백 함수 방식으로 전달되고 있다. userData.setName 코드를 보듯이 setName 함수가 userData 객체 안에 있다는 것을 나타내기 때문에 일반적으로 setName 함수 내의 this 키워드는 당연히 userData 객체를 가리키는 것으로 이해하게 된다.

function getUserName(firstName, lastName, callback) {
    callback(firstName, lastName);
}

getUserName('홍', '길동', userData.setName);

console.log('1: ', userData.name); // Not Set
console.log('2: ', window.name); // 홍 길동

 

그러나 콜백 함수로 전달될때는 상황이 다르다. 생신 건 객체 참조의 메서드의 모양으로 들어가 있지만, 사실 객체의 메서드를 그대로 넘긴 것이 아니라 그냥 콜백 함수의 역할을 하는 함수로써 넘긴 것 뿐이다.
즉, 이 상황을 풀어보면 아래와 같이 되는 것이다.

let userData = {
    signUp: '2021-4-06 12:00:00',
    name: 'Not Set',
    setName: function(firstName, lastName) {
        this.name = firstName + ' ' + lastName;
    }
}

function getUserName(firstName, lastName, callback) {
    callback(firstName, lastName);
}

// 해당 콜백함수는 userData.setName과 아무런 관계가 없는 함수 형태만 비슷한 독립적인 함수이다.
getUserName('홍', '길동', function(firstName, lastName) {
    this.name = firstName + ' ' + lastName;
});

console.log('1: ', userData.name); // Not Set
console.log('2: ', window.name); // 홍 길동

userData 객체와 아무런 연관성이 없는 독립적인 메서드이기 때문에 콜백 함수 내의 this는 userData 객체를 가리키지 않는 것이다. 그리고 일반적으로 콜백 함수를 호ㅜㄹ하는 함수들은 별도로 this를 설정하지 않으므로 기본적으로 전역 객체가 바인딩 되는 것이다.



콜백 함수 this 해결 방안

따라서 콜백 함수 내의 this를 보호할 수 있도록 콜백 함수를 만들어야 한다.

 

  1. call, bind, apply 메서드 사용

call(), apply()를 사용하여 this를 보호할 수 있다.

  • call() : 첫 번째 인자로 this 객체 사용, 나머지 인자들은 ,로 구분
  • apply() : 첫 번째 인자로 this 객체 사용, 나머지 인자들은 배열 형태로 전달

 

솔루션 원리는 간단하다. 참조할 객체를 추가로 함수의 매개변수로 전달하고, 콜백 함수 내에서 call(), apply() 메서드를 통해 콜백 함수가 참조할 객체를 지정해주면 된다.

// this 대신 userData를 사용하는 방법
let userData = {
    signUp: '2021-4-06 12:00:00',
    name: 'Not Set',
    setName: function(firstName, lastName) {
        this.name = firstName + ' ' + lastName;
    }
}

function getUserName(firstName, lastName, callback, data) { // userData를 받는 매개변수 data를 추가
    callback.call(data, firstName, lastName); // data를 this로 사용
}

getUserName('홍', '길동', userData.setName, userData); // userData를 인수로 전달

console.log('1: ', userData.name); // 홍 길동
console.log('2: ', window.name); // Not Set
// this 대신 userData를 사용하는 방법
let userData = {
    signUp: '2021-4-06 12:00:00',
    name: 'Not Set',
    setName: function(firstName, lastName) {
        this.name = firstName + ' ' + lastName;
    }
}

function getUserName(firstName, lastName, callback, data) { // userData를 받는 매개변수 data를 추가
    callback.apply(data, [firstName, lastName]); // data를 this로 사용하고 배열을 전달
}

getUserName('홍', '길동', userData.setName, userData); // userData를 인수로 전달

console.log('1: ', userData.name); // 홍 길동
console.log('2: ', window.name); // Not Set

 

  1. 화살표 함수 사용

화살표 함수는 자신만의 this를 가지지 않고 상위 스코프의 this를 참조하기 때문에 전역 객체를 무시하고 무조건 자신을 들고 있는 상위 객체를 가리킨다.

let userData = {
    signUp: '2021-4-06 12:00:00',
    name: 'Not Set',
    setName: (firstName, lastName) => { // 화살표 함수로 변경
        this.name = firstName + ' ' + lastName;
    }
}

function getUserName(firstName, lastName, callback) {
    callback(firstName, lastName); // call 메서드 없이 호출
}

getUserName('홍', '길동', userData.setName);

console.log('1: ', userData.name); // 홍 길동
console.log('2: ', window.name); // Not Set



콜백 지옥(CallBack Hell)

콜백 지옥이란 함수의 매개변수로 넘겨지는 콜백 함수가 반복되어 코드의 들여쓰기 수준이 감당하기 힘들어질 정도로 깊어지는 현상

function add(x, callback) {
    let sum = x + x;
    console.log(sum);
    callback(sum);
}

add(2, function(result) {
    add(result, function(result) {
        add(result, function(result) {
            console.log('finish!!');
        })
    })
})

/*
4
8
16
finish!!
*/



자바스크립트 비동기와 콜백

자바스크립트는 싱글 스레드 언어로, 하나의 작업만을 동시에 처리할 수 있다. 즉, JS는 코드를 위에서 아래로 순차적으로 실행한다.
그리고 웹 개발에서는 네트워크 요청이나 타이머 등의 작업이 필요한 경우가 많은데, 이러한 작업들은 시간이 오래 걸리거나 결과가 불확실하다.
그래서 JS는 비동기(asynchronous) 방식으로 작업을 처리하는 기법을 제공한다.

 

 

비동기란 현재 실행중인 작업을 멈추지 않고 다른 작업을 병렬적으로 수행하는 것을 의미한다.
그래서 여러 작업(task)이 있을 때 비동기적으로 수행하면 작업들을 동시에 한번에 수행할 수 있어 위 사진 처럼 결과적으로 최종 작업 수행이 빠르게 처리된다. 그리고 이러한 비동기 방식으로 작업을 처리하는 방법 중 하나가 바로 콜백(callback) 함수이다.

 

출처

https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%BD%9C%EB%B0%B1-%ED%95%A8%EC%88%98

https://velog.io/@ko1586/Callback%ED%95%A8%EC%88%98%EB%9E%80-%EB%AD%94%EB%8D%B0

'JS' 카테고리의 다른 글

Async/Await  (0) 2024.06.12
동기, 비동기  (1) 2024.06.12
Binding의 개념과 call, apply, bind  (1) 2024.06.08

binding이란?

this를 binding한다. 이 말이 무슨 뜻일까?
javascript의 함수는 각자 자신만의 this라는 것을 정의한다. 예를 들어 자기소개를 하는 함수를 위해 say()이라는 함수를 만든다고 하자.

const say = function(){
    console.log(this);
      console.log("Hello, my name is" + this.name);
}

say();

실행결과는 다음과 같다.

window객체가 나타난다. 기본적으로 this는 window이기 때문이다.
하지만 무조건적으로 window라고 볼 수는 없다. this는 객체 내부, 객체 메서드 호출 시, 생성자 new 호출 시, 명시적 bind 시에 따라 바뀌기 때문이다.

this의 binding을 통해서 this를 원하는, 알맞은 객체로 바꿔줄 수 있다.
명시적으로 위의 this를 window가 아닌 다른 객체로 바꿔주는 함수가 call, apply, bind이다.

call과 apply

원래 함수는 선언한 후 호출해야 실행된다.
호출하는 방법으로는 함수 뒤에 ()를 붙이는 것, 그리고 call과 apply하는 방법이 있다.

다음은 say 함수이다.

cosnt say = function(city){
    console.log(`Hello, my name is ${this.name}, I live in ${city}`);
} 

const obj = {name:"Tom"};

say("seoul"); // Hello, my name is , I live in seoul
say.call(obj, "seoul"); // Hello, my name is Tom, I live in seoul
say.apply(obj, ["seoul"]); // Hello, my name is Tom, I live in seoul

call과 apply는 함수를 호출하는 함수이다. 첫 번째 인자에 this로 설정하고 싶은 객체를 넘겨주어 this를 바꾸고나서 실행한다.

첫 번째 실행의 경우 this에 아무런 setting이 되어있지 않으므로, this는 window 객체이다. 두 번째, 세 번째 실행의 경우 this를 obj로 변경시켰으므로 원하는 값이 나온다.

call과 apply의 첫 번째, this를 binding할 값을, 나머지에는 실제 say에 필요한 parameter를 입력하는 방식이다. call과는 다르게 apply함수는 두 번째 인자부터 모두 배열에 넣어야 한다.


또 다른 예이다.

var obj = {
  string: 'zero',
  yell: function() {
    console.log(this.string);
  }
};
var obj2 = {
  string: 'what'
};

obj.yell(); // 'zero';
obj.yell.call(obj2); // 'what', obj1.yell()을 obj2.yell()로 바꾼 효과라고 보면 된다
obj.yell.apply(obj2); // 'what'

위의 코드의 obj.yell.call(obj2)로 this가 가리키는 것을 obj에서 obj2로 바꾸었다.

yell은 obj의 method임에도 불구하고 zero 대신 what으로 변경되었다.

즉 call을 써서 this를 정의해준다면 다른 객체의 parameter나 method를 자기 것마냥 사용할 수 있다.

<팁!>

  • .call(this, paramter1, parameter2, ...)
  • .apply(this, [paramter1, parameter2, ...])

bind

bind 함수가 call, apply와 다른 점은 함수를 실행하지 않는다는 점이다. 대신 bound 함수를 리턴한다. 이 bound 함수는 this를 설정한 함수이다. 또한 함수에 argument를 넣어 줄 수 있다.

const obj = {name: "nanana"};

const say = function(city){
    console.log(`Hello, my name is ${this.name}, I live in ${city}`);
}

const boundSay = say.bind(obj);
const boundSayTwo = say.bind(obj,"busan");

boundSay("seoul");
boundSayTwo();

call, apply, bind 정리

call, apply, bind는 함수 호출 방법을 지정해서 this를 그때 그때 알맞은 객체로 명시적으로 바꿔주는 메소드이다.

call, apply와 bind의 차이점

  • call, apply는 함수를 호출해서 인수 전달.
  • bind 함수는 호출하지 않는다. this값을 유지하는 새로운 함수를 생성

call과 apply의 차이점

  • call의 경우, 호출한 함수의 인수를 ,로 구분한 리스트 형식으로 전달
  • apply는 배열 [ ]로 전달

출처

https://wooooooak.github.io/javascript/2018/12/08/call,apply,bind/
https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-Call-Bind-Apply
https://velog.io/@dev_0livia/JavaScriptcall-apply-bind%EC%9D%98-%EC%B0%A8%EC%9D%B4

'JS' 카테고리의 다른 글

Async/Await  (0) 2024.06.12
동기, 비동기  (1) 2024.06.12
Call back 함수  (1) 2024.06.11

+ Recent posts