약 빨고 쓴 콜백 함수(callback)

조 은길·2021년 2월 28일
3

Javascript 정리

목록 보기
27/48
post-thumbnail
post-custom-banner

비동기의 핵심 콜백함수?? 댓츠 노노!!

부트 캠프 강의를 들을 때도 그렇고, 유명한 유튜브 영상에서도 비동기의 핵심이 콜백함수라고 가르친다.
지금에서야 다시금 깨달은 거지만, 이 말은 틀렸다.
비동기함수에 콜백함수가 종종 쓰일 뿐이거지. 콜백함수와 비동기는 아무 상관이 없다.
콜백 함수는 그저 디자인 패턴일 뿐이니까...

지금부터 이런 엽기적인 설명들 때문에 헷갈렸던 나 자신을 되돌아보며, 콜백함수에 대해서 제대로 알아보자!!


JS는 항상 "동기적"으로 처리한다

자바스크립트는 동기적이다.
=> 호이스팅이 된 이후부터, 코드가 우리가 작성한 순서에 맞춰서 하나씩 동기적으로 실행되다는 말이다.

<script>
  console.log(1); 
  console.log(2); 
  console.log(3);
</script>

쉬운 예로 위의 코드를 실행해보면, 1->2->3 순서로 실행되는 것을 알 수있다.

이렇게 위에서 아래로 코드가 순서대로 실행되는 것을 동기적이다~ (synchronous) 라고 한다.

그냥 거의 대부분의 프로그래밍 언어들은 이런 특징을 가지고 있다.


근데, JS는 뭐가 잘났다고 "비동기"드립을 치냐??

JS는 단순히 코드 실행만 하는게 아니라, 웹페이지 렌더링 역시 담당한다.

예를 들어, 위에서 아래로 코드가 실행되던 중에

container.addEventListener("click", clickAfter);
console.log(1);
setTimeout(function(){}, 1000);
console.log(2);

addEventListener 함수를 만났다면, JS는 동기적이기 때문에, 클릭이 될 때까지 JS는 멈춰버린다.
다시 말해서, container.addEventListener("click", clickAfter);가 실패든 성공이든 어떻게든 해결이 될 때까지 "JS는 결과값이 나올 때까지" 그 라인에서 마냥 기다리고 있다.

  • 자!! 그럼, 웹 페이지를 사용하는 유저 입장에서는 기분이 어떨까??

웹페이지 UI를 뿌려줘야 될 JS가 클릭이 될지 안 될지도 모르는 라인 하나 때문에, 마냥 대기타고 있는데...

부들부들...

"JS의 실행기"인 브라우저는 이런 말도 안 되는 상황을 보고만 있을 수없었다.

그래서, 브라우저는 이런 오래 걸리는 혹은, 언제 결과값이 나올지 모르는 작업들을 따로 모아다가 Web API라는 곳에 넣어둔다.

그리고 결과값이 받아와지면, 다시 메인 무대(콜 스택)으로 복귀해서 작업을 수행한다.

  • 우리는 이것을 "비동기"라고 부른다.

브라우저들의 이런 천재성 덕분에 addEventListener가 나오든, setTimeout이 나오든 빠꾸없이, 코드를 쭉쭉 실행할 수 있게 됐다.

console.log(1);
setTimeout(function(){ console.log("메롱") }, 5000);
console.log(2);
// 1 -> 2 -> "메롱"

브라우저가 없었으면, JS는 1 찍고 => 5초 ㄱㄷ렸다가 "메롱" 찍고 => 2를 찍는 대형 참사가 벌어졌을 거다.

물론, 코드를 짜는 개발자 입장에서는 이게 더 직관적이고, 이해하기 쉽지만, 우리는 어디까지나 "JS는 웹페이지를 그려주는 역할을 담당한다."는 사실을 명심해야 한다.

5초 ㄱㄷ렸다가 다음 라인으로 넘어가면, 웹페이지를 기다리는 유저는 5초를 더 기다려야 웹페이지를 볼 수있는 거다.

정리하면,

  • JS는 다른 언어들과 마찬가지로 동기적으로 실행된다.
    • 그러나, JS는 웹 페이지를 렌더링해주는 특수한 역할을 수행하는 언어이다.
    • 그래서, 마냥 코드를 순차적으로 실행할 수만은 없다.
  • JS의 실행기인 브라우저는 이런 문제를 해결하고자 비동기하는 방식으로 오래걸리는 코드들을 처리해준다.
  • 즉, 콜백함수를 쓴다고 비동기가 되는게 아니라,
    JS에서 비동기적으로 코드를 실행하고 싶다면, Web API에서 처리해주는 함수들을 쓰는 방법밖에 없다.
  • 또다른 "JS의 실행기"인 Node.js 역시 브라우저와 동일하게 JS를 처리한다. (비동기 쓴다고!!)

그럼, 이 놈의 Callback 함수는 왜 필요한겨??

설령, 아무리 오래걸린다고 해도 반드시 특정 코드가 실행된 이후에야, 그 다음 코드가 실행되어야만 하는 상황이 있다.
=> 다시 말해서, 코드가 반드시 순서대로 실행되어야 하는 상황이 있다는 뜻이다.

필자가 부트캠프에서 진행했던 날씨 API Sprint 예시를 들어보자!!

  • 아래 코드는 필자가 실제로 겪었던 시행착오이다.

function renderWeatherData() {
  // TODO: 여기에 DOM을 이용하여 날씨 데이터를 표시하세요

  const json = getData(); // 문제의 발생 지점!!

  console.log("this is ", json); // =>  undefined

  console.log(json.weather[0]["main"]); // => error
  console.log(json.weather[0]["description"]); // => error
}

function getData() {
  fetch(API_URL_OpenWeatherMap)
    .then(function (resp) {
      return resp.json();
    })
    .then(function (json) {
      // TODO:
      // 요청이 완료되고 나면 여기서부터 날씨 데이터(json)를 사용할 수 있습니다.
      // 하드코딩된 data를 대체하세요.

      return json;
    });
}

renderWeatherData();

fetch()가 비동기 방식이기 때문에 const json = getData();으로 할당하려고 해도 undefined가 뜬다. 다시 말해서, 데이터를 받아온 다음에 json이라는 변수에 할당해줘야하는데, getData(); 함수 내에 비동기적으로 처리되는 녀석이 있어서 Web API로 들어가버린다.
그러면, 브라우저는 const json = getData();을 일단 건너뛰고 그 다음 줄부터 실행해버린 것이다.

그 결과, 데이터 값을 못 받아온 채로 UI를 뿌려줘서 웹페이지가 제대로 렌더링 되지 못했다.

  • 이 경우라면, 코드는 반드시 순차적으로 실행되어야 한다.
    그러나, 비동기 함수가 나타나면, 브라우저는 반드시 Web API로 넘기고, 다음 라인을 실행한다.
    그렇다면, 개발자 입장에서는 강제로 코드를 순서대로 실행하게 만드는 방법밖에는 없다.

바로, 이때 사용하는 디자인 패턴이 바로... "콜백 함수"이다!!


JS를 순차적으로 실행하려면??

자바스크립트에서 1 => 2를 출력하는데, 그 사이에 1초 텀을 두는 코드를 짜보자.

  • 이런다고 1초 쉬고, 2가 출력되지는 않는다.
console.log(1);
setTimeout( ()=>{ }, 1000);
console.log(2);
  • 정답
console.log(1);
setTimeout( ()=>{ console.log(2); }, 1000);

이렇게, JS에서 코드를 순차적으로 실행시키고 싶을 때, 콜백함수 ()=>{ console.log(2); }를 하나 만들고, 그 안에 기능 개발을 한다.

그러면, 순차적으로 실행이 된다.


CallBack 함수

콜백 함수는 함수 안에 파라미터로 들어가는 함수이다.
또한,
=> 성공의 결과를 받아줄 수있는 함수 이다.

용도 : JS에서 뭔가를 순차적으로 실행하고 싶을 때, 자주 쓰는 디자인 패턴

특징

  • 다른데서 만든 함수도 콜백함수로 집어넣을 수있다.
document.querySelector('.button').addEventListener('click', 함수명);
  • 콜백함수가 필요한 함수들에만 콜백함수 사용 가능하다.
    ex) map, filter, addEventListener 등등

콜백 함수 작동 원리

여기까지 읽고 이해한 우리들은 이제 아래의 코드가 항상 순서대로 작동하지 않는다는 사실을 안다.

first();
second();

그럼, 이 코드를 강제로 반드시 순차적으로 실행하게 만들어보자!!

function first( a ){
  console.log('first');
  a();
}

function second(){
  console.log('second');
}

// 이렇게하면, first() 다음에 second()가 실행되도록 할 수 있다. 
first(second);

함수에 이름을 짓는 경우

일반적으로 함수에 이름을 짓는 경우는 그 함수의 기능을 다시 쓸 일이 있어서이다.

그러나, 모든 함수들의 재사용성이 높지는 않다. 즉, 함수의 이름을 지을 필요가 없는 것들도 많다.

=> 다시 쓸 일이 있는 함수들만 이름을 짓는다.
=> 콜백 함수로 쓰는 함수들은 이름을 안 짓는 경우도 많다.


콜백 함수로 "함수선언문"을 집어넣을 수도 있다

위의 경우처럼 재사용성이 있는 미리 만들어놓은 함수를 집어넣을 수도 있고,


첫째함수(function(){
  console.log(2)
}):

이렇게 직접 함수선언문을 집어넣을 수도 있다.

콜백 함수의 단점

그러나, 순차적으로 실행하려고 콜백함수를 여러개 사용하면 단점이 조금 있다.

코드가 옆으로 길어진다.


첫째함수(function(){
  둘째함수(function(){
    셋째함수(function(){
      어쩌구..
    });
  });
}):

첫째함수 둘째함수 셋째함수 이렇게 차례로 실행해주는 코드이다.

근데, 이렇게 되면, 코드가 너무 우측으로 길어진다.
별 수 없다. JS가 이렇게 생겨먹은 것을 어쩌겠나...
=> 특히 자바스크립트로 서버개발시, 이런 패턴이 흔하다.

이런 식으로 콜백 함수를 계속 사용하다보면, 코드가 너무 길어져서 그것을 읽는 사람이 지옥을 맛 본다는 뜻으로 콜백 지옥이라는 이름이 붙었다.

콜백지옥의 문제점
1. 가독성이 떨어진다.
2. 가독성이 떨어지면, 에러가 발생했을 때 디버깅하기가 어렵다.
3. 체인이 길어지면 길어질수록, 문제를 분석하고 디버깅하기 어렵다.
4. 결국, 유지보수도 어렵다 ㅠ.ㅠ

이런거 보기싫으시면 ES6 신문법인 Promise라는 기계를 만들어 사용하면 된다.

콜백대신 쓸 수 있는 Promise 디자인 패턴을 적용하면 어떻게 되냐면


첫째함수().then(function(){
   그 담에 실행할거
}).then(function(){
   그 담에 실행할거
});

옆으로 길어지지 않고 .then()이라는 키워드 덕분에 그나마 뭘 하는지도 파악이 쉬워진다.


콜백 함수를 집어넣을시, 주의사항

document.querySelector('.button').addEventListener('click', 함수명);
  • 함수명함수명() 이렇게 넣으면 안 된다.
    왜냐면, 이건 "함수를 실행해주세요."라는 의미이다. 클릭이 일어나지도 않았는데, 함수를 바로 실행하면 안되니까요!!
document.querySelector('.button').addEventListener('click', function(){
  alert('깔깔깔깔깔깔!!!'); // 버튼을 클릭시, 이거 실행됨
  // 순차적으로 실행되죠?? 콜백이니까...
})

이 경우 역시도 익명 함수를 할당해놓은 것뿐이지 실행시켜놓은게 아니다.

  • 함수명()function(){...}() 둘은 같은 의미이다.

콜백은 협업에서도 유용하다

협업을 했을 때는 콜백의 강력함이 발휘된다.

안정적으로 확실하게 특정 코드를 순차적으로 실행할 수 있다.

예를 들어, 필자가 first()를 만들었는데, 너무 유용해서 다른 사람들이 갖다 쓴다고 하자.

그런데, 개발자 A는 first() 이후에 console.log(1)을 실행하고 싶다면 어떻게 해야 할까??

직관적으로는

first();
console.log(1);

이렇게 구현할 수 있겠지만, first()가 비동기처리가 되면 순서대로 작동하지 않는다.

확실한 안전빵으로 first()를 업그레이드 하면 된다.
first( 파라미터 )에 파라미터를 추가하고,

function first( 파라미터 ){
  console.log('first');
  파라미터();
}

function second() {
  console.log(1);
}

first(second);

이렇게 콜백 함수 패턴을 사용하면, 안전하게 순차적으로 원하는 코드를 실행시킬 수 있다.


콜백 함수는 비동기와 상관 없다

비동기함수들만 콜백 함수 디자인 패턴을 쓰는 것은 아니다.
동기적인 함수 내에서도 콜백 함수 패턴을 쓸 수 있는데, 이런 경우를 잘 못보는 이유는 구지 이렇게 쓸 필요가 없어서이다.

이 사실만 염두해두고 아래의 예시들을 보면, 크게 어렵지 않다.

Case 1 ) 동기적 함수 내의 콜백 함수

위의 fakeSetTimeout() 콜백 함수는 함수가 불려진 순서대로 출력 된다.

Case 2 ) 비동기적 함수 내의 콜백 함수

이제 대표적인 비동기 함수인 setTimeout()을 써보자!
=> 아니! 좀 더 정확하게 말해서, 콜백 함수라는 디자인 패턴을 가지고 있는 비동기 함수인 setTimeout()을 써보자!

=> 뭔가 이상하다. 분명히 setTimeout() 함수의 작동시간도 0초로 line 14에 만들어 줬는데... 가장 마지막에 작동된다. 왜 이럴까??

  • 이미 여기까지 읽은 분들이라면, 당연히 알고 계셔야 할겁니다.

그러나, 좀 더 디테일한 원리를 알고 싶다면, JS 비동기의 핵심, 이벤트 루프를 보길 바란다.


참고자료 및 자료출처

profile
좋은 길로만 가는 "조은길"입니다😁
post-custom-banner

2개의 댓글

comment-user-thumbnail
2022년 6월 13일

잘 보고 갑니다. 콜백에 대한 사무친 한이 느껴지네요ㅋㅋㅋㅋ

1개의 답글