Javascript

[Javascript] 콜백 함수와 Promise

YunCod 2024. 2. 6. 15:16

콜백 함수(Callback Function)

function repeatFunc(n, func) {
  for (let i = 0; i < n; i++) {
    func(i);
  }
}

function odd(i) {
  if (i % 2) console.log(i);
}

function even(i) {
  if (i % 2 == 0) console.log(i);
}

repeatFunc(5, odd)	// 1, 3
repeatFunc(5, even)	// 0, 2, 4

콜백함수 예시

  • 매개변수를 통해 다른 함수의 내부로 전달되는 함수
  • 매개변수를 통해 함수의 외부에서 전달받은 콜백 함수를 고차 함수라고 함

 

 

콜백 함수의 사용

1. 함수의 구조적 설계

  • 콜백 함수를 사용하여 로직 처리 부분과 나머지 부분을 구분할 수 있으며, 이를 통해 함수의 역할을 나누고 재사용하는 등 구조적인 함수 설계가 가능해짐
function benchPress(day) {
  console.log('벤치프레스');
}

function deadLift() {
  console.log('데드리프트');
}

function squat() {
  console.log('스쿼트');
}

function today(day) {
  console.log(`${day}의 운동 : `);
}

function todayWorkout(day) {
  if (day === '월요일') {
    today(day);
    benchPress();
  } else if (day === '화요일') {
    today(day);
    deadLift();
  } else if (day === '수요일') {
    today(day);
    squat();
  }

  // 목요일... 금요일... 토요일...
}

todayWorkout('월요일');
todayWorkout('화요일');

목요일, 금요일, 토요일,.. 다른 요일의 루틴을 추가하려면 else if문을 추가해야함.

화요일의 루틴을 변경하고 싶으면 todayWorkout함수의 else if문에 있는 squat()함수를 직접 바꿔야함.

 

function benchPress(day) {
  console.log('벤치프레스');
}

function deadLift() {
  console.log('데드리프트');
}

function squat() {
  console.log('스쿼트');
}

function today(day) {
  console.log(`${day}의 운동 : `);
}

function todayWorkout(day, routine) {
  today(day);
  routine(day);
}

todayWorkout('월요일', benchPress);
todayWorkout('화요일', deadLift);

루틴을 추가하려면 todayWorkout함수에 요일과 루틴 함수를 입력하여 호출하면 됨.

화요일의 루틴을 변경하고 싶으면 todayWorkout의 두 번째 인자에 다른 함수를 입력.

 

 

2. 비동기 처리

  • 자바스크립트는 코드를 위에서부터 순차적으로 실행하지만, 실제로 실행 시켰을 때 순서가 보장되지 않는 경우가 있음.
  • 함수의 비동기 처리 결과에 대한 후속 처리를 수행하기 위한 용도로 콜백 함수가 사용된다.
  • 대표적인 비동기 처리
    • setTimeout(타이머 함수) : 두 번째 인자로 받은 만큼의 ms가 지난 이후 첫 번째 인자로 받은 콜백 함수를 실행
    • fetch(API 호출 함수) : api를 호출의 결과를 콜백 함수로 받아 then, catch메서드로 후속 처리를 수행
// 1초 후에 콜백 함수가 실행됨
setTimeout(function () {
  console.log('Time is up!');
}, 1000);

console.log('내가 setTimeout 함수 이후에 출력될까?');
console.log('아닌데ㅋ');

타이머 함수 예시

타이머 함수 출력 결과

// fetch메서드를 이용한 API호출
fetch('https://url')
  .then(function (response) {
    // fetch 메서드가 성공하면 콜백 함수에 response인자를 받음
    return response.json();
  })
  .then(function (data) {
    // 위의 then에서 리턴받은 json 메서드가 성공하면 콜백 함수로 data인자를 받음
    console.log(data);
  })
  .catch(function (error) {
  	// fetch, then단계 어디서든 에러가 발생하면 catch구문에 도달하여 콜백 함수로 error인자를 받음
  	console.log(error);
  });

fetch 함수 예시

 

3. 이벤트 리스너

  • 클릭과 같은 html 이벤트가 발생한 이후 콜백 함수를 실행
let button = document.getElementById("button");
button.addEventListener("click", function () { 
  console.log("클릭"); // 콜백 함수에서 출력문을 실행
});

addEventListener 메서드 예시

 

4. 고차함수에서의 사용

  • forEach(), map(), find() 와 같은 배열 고차함수에서, 배열의 각 요소에 대해 콜백 함수를 실행하고 결과를 반환
const nums = [1, 2, 3, 4];
let sum = 0;

// forEach메서드
nums.forEach((num) => {
  sum += num;	// 배열을 순회하며 콜백 함수를 수행
});
console.log(sum);   // 10

// map메서드
const numMap = nums.map((num) => {
  return num + 2;	// 배열을 순회하며, 최종적으로 각 요소마다 return문을 수행된 결과 배열을 반환
});

console.log(numMap);  // (4) [3, 4, 5, 6]

forEach(), map()메서드 예시

 

 

Promise 객체의 등장 배경

  • 비동기 함수는 비동기 처리의 결과를 외부에 반환할 수 없고, 상위 스코프의 변수에 할당할 수도 없다. 결국 비동기 함수의 내부에서 후속 처리를 수행할 수 밖에 없다.
// 쇼핑몰의 판매탭에서 상품의 id를 가져오고
get('https://.../shopping/sales/1', (goodsId) => {
  // 상품의 id로 상품의 정보를 찾고
  get(`https://.../shopping/goods/${goodsId}`, (goodsInfo) => {
    // 상품 정보에서 또 다른 무엇인가를 찾고..
    get(`https://.../shopping/someWhere/${goodsInfo}`, (someId) => {
      // 다른 url로 이동해서 또..
      get(`https://.../shopping/anyWhere/${someId}`, (something) => {
        console.log(something);
      });
    });
  });
});

콜백 헬 예시

  • VanillaJS로 API 요청 코드를 작성하면 훨씬 복잡하지만, 간단하게 get('url주소', function(인자) {이후 수행될 콜백 로직})이라는 함수를 만들었다고 치자.
    위의 예시와 같이 요청이 성공하면 응답을 가져오고, 그 응답을 이용해서 또 새로운 요청을 하고... 가 반복되다보면 들여쓰기가 반복되어 가독성이 떨어지며, 실수를 유발하는 원인이 된다. => 콜백 헬
fetch('https://url')
  .then(function (response) {
    // fetch 메서드가 성공하면 콜백 함수에 response인자를 받음
    return response.json();
  })
  .then(function (data) {
    // 위의 then에서 리턴받은 json 메서드가 성공하면 콜백 함수로 data인자를 받음
    console.log(data);
  })
  .catch(function (error) {
    // fetch, then단계 어디서든 에러가 발생하면 catch구문에 도달하여 콜백 함수로 error인자를 받음
    console.log(error);
  });

Promise Chaining을 이용한 코드

  • 이를 해결하기 위해 ES6버전에서 Promise객체가 등장하였고, Promise의 후속 처리 메서드인 then, catch, finally를 이용하여 깔끔한 코드를 작성할 수 있다.

 

 

Promise 객체

프로미스?

1. Promise의 상태

Pending(대기)

const promise = new Promise(function (reslove, reject) {});
console.log(promise);   // Promise {[[PromiseState]]: 'pending', [[PromiseResult]]: undefined, ...
  • new Promise() 메서드로 호출할 때 콜백 함수를 선언할 수 있고, 콜백 함수의 인자는 resolve, reject
  • 처음 호출 시 대기 상태(Pending)가 되며, 결과 값은 undifined.

Fulfilled(이행)

const promise = new Promise(function (reslove, reject) {
  const data = 'resolved data';
  reslove(data);
});

promise
  .then(function (data) {
    console.log(data);  // resolved data
  });
  • 콜백 함수의 인자인 resolve함수를 실행하면 이행(Fulfilled) 상태가 되며, reslove함수의 인자가 결과 값으로 할당됨
  • 이행 상태가 되면 then() 메서드를 이용하여 처리 결과를 콜백 함수로 받을 수 있다.

Rejected(실패)

const promise = new Promise(function (reslove, reject) {
  reject(new Error('요청 실패ㅠㅠ'));
});

promise
  .then(function (data) {
    console.log(data);
  })
  .catch(function (error) {
    console.log(error);   // Error: 요청 실패ㅠㅠ ...
  });
  • 콜백 함수의 인자인 rejected함수를 실행하면 실패(Rejected) 상태가 되며, reject함수의 인자가 결과 값으로 할당됨
  • 실패 상태가 되면 catch() 메서드를 이용하여 처리 결과를 콜백 함수로 받을 수 있다.

 

2. Promise Chaining

콜백 헬 해결!

fetch(
  'https://api.thecatapi.com/v1/images/search?size=med&mime_types=jpg&format=json&has_breeds=true&order=RANDOM&page=0&limit=1'
)
  .then(function (response) {
    return response.json();
  })
  .then(function (data) {
    console.log(data[0].url);
  })
  .catch(function (error) {
    console.log(error);
  })
  .finally(function () {
    console.log('난 성공하든 실패하든 일단 출력된단다.');
  });
  
  // 출력 결과
  // https://cdn2.thecatapi.com/images/FUJOW3SIi.jpg
  // 난 성공하든 실패하든 일단 출력된단다.
  • Promise객체의 등장 배경에서 살펴본 바와 같이 then(), catcth(), finally() 메서드를 이용하여 콜백 헬을 해결할 수 있다.
  • 이처럼 연속적인 호출이 가능한 이유then(), catcth(), finally() 메서드가 모두 Promise 객체를 반환하기 때문이다.

 

3. Promise.all()

여러 개의 비동기 처리를 병렬적으로 처리하는 메서드

  • Promise.all() 메서드는 인자를 Promise객체를 요소로 갖는 배열로 받아서 비동기 작업을 병렬적으로 처리한다.
  • 인자로 전달받은 배열의 모든 Promise가 fulfilled상태가 되면 종료한다.
    따라서 모든 처리에 걸리는 시간은 가장 늦게 fulfilled상태가 되는 작업보다 조금 길다.
  • 배열의 Promise중 하나라도 rejected 상태가 되면 나머지가 fulfilled상태가 되는 것을 기다리지 않고 즉시 종료한다.

Cat API를 활용한 예제

예제를 위해 https://thecatapi.com/의 API를 사용하였습니다.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Cat Image</h1>
    <div id="imgBox"></div>
  </body>
  <script>
    function insertImg(url) {
      const imgBox = document.getElementById('imgBox');
      const img = new Image();
      img.src = url;
      img.width = 300;
      img.height = 200;
      imgBox.appendChild(img);
    }

    const getCat = function () {
      return fetch(
        'https://api.thecatapi.com/v1/images/search?size=med&mime_types=jpg&format=json&has_breeds=true&order=RANDOM&page=0&limit=1'
      )
        .then(function (response) {
          return response.json();
        })
        .then(function (data) {
          const imgSrc = data[0].url;
          insertImg(imgSrc);
        });
    };

    // Promise Chaing을 이용하여 비동기 요청이 순차적으로 수행됨
    getCat()
      .then(function () {
        return getCat();
      })
      .then(function () {
        return getCat();
      });
  </script>
</html>

  • Promise Chaining을 이용하여 작성한 코드는 개발자 도구의 네트워크창을 통해 확인해보았을 때, 이미지를 받아오고 그리는 작업이 순차적으로 수행된 것을 볼 수 있었다.

 

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Cat Image</h1>
    <div id="imgBox"></div>
  </body>
  <script>
    function insertImg(url) {
      const imgBox = document.getElementById('imgBox');
      const img = new Image();
      img.src = url;
      img.width = 300;
      img.height = 200;
      imgBox.appendChild(img);
    }

    const getCat = function () {
      return fetch(
        'https://api.thecatapi.com/v1/images/search?size=med&mime_types=jpg&format=json&has_breeds=true&order=RANDOM&page=0&limit=1'
      )
        .then(function (response) {
          return response.json();
        })
        .then(function (data) {
          const imgSrc = data[0].url;
          console.log(imgSrc);
          return imgSrc;
        });
    };
	
    // Promise.all을 사용하여 비동기 요청이 병렬적으로 수행됨
    Promise.all([getCat(), getCat(), getCat()]).then((dataList) => {
      dataList.forEach((imgSrc) => {
        insertImg(imgSrc);
      });
    });
  </script>
</html>

 

  • Promise.all() 메서드를 사용했을 때에는 이미지를 받아오고 그리는 작업이 병렬적으로 수행되었다.