[JavaScript] 콜백함수란?

매튜·2023년 2월 18일

JavaScript

목록 보기
5/9

CallBack function(콜백함수)

콜백함수는 함수의 파라미터로 함수가 들어가는 함수이다.

function sayHello(name) {
  console.log(`Hello, my name is ${name}`);
}

function name(callback) {
  callback('Salah');
}

name(sayHello); // Hello, my name is Salah

첫번째 함수는, 일반적인 name 이라는 파라미터를 받아서 인사내용을 출력하는 함수입니다. 두번째 함수는, 함수를 파라미터로 받아 내부에서 함수를 실행시키는 콜백 함수입니다.
name(sayHello)는 name이라는 함수에 sayHello라는 함수를 인자로 전달합니다. name함수 안에서 받은 콜백 함수는 sayHello함수를 실행시키고 전달받은 인자 'Salah'를 해당 함수 내부의 파라미터 값으로 사용합니다.

콜백함수를 쓰는 이유?

콜백함수를 사용하면 순차적으로 자신이 실행하고 싶은 코드가 있을 때 순차적으로 실행시킬 수 있습니다. 자바스크립트는 동기적이기에 코드를 한줄씩 번역해가며 실행합니다. 그러나 중간에 setTimeout이나 eventListener같이 순차적으로 실행해야하는 코드가 존재하면 그떄 콜백함수를 사용합니다.

function first() {
  setTimeout(() => {
    console.log("0초 뒤에 실행!");
  }, 0);
}

function second() {
  console.log("두번째로 실행!");
}

first(); 
second();

// 두번쨰로 실행!
// 0초뒤에 실행!

우리가 원하는 출력값은 first가 먼저 출력되고, 그다음 second가 출력되는것을 원했습니다.

원하는대로 출력되지 않는 이유는?
first 함수 내부에서 setTimeout을 사용했기 떄문입니다. 즉, setTimeout이나 eventListener와 같은 함수들은 특정 조건(click, n초 후 실행)을 만족해야 내부에 있는 코드를 실행합니다. 그 사이에 second 함수가 먼저 실행됩니다.

first -> second 순으로 함수 실행하기.

이떄 콜백함수를 사용한다.

function first(callback) {
  setTimeout(() => {
    console.log("0초 뒤에 실행!");
    callback();
  }, 0);
}
function second() {
  console.log("두번째로 실행!");
}
first(second);
// 0초 뒤에 실행!
// 두번째로 실행!

first 함수의 파라미터로 second 함수를 넣었고 first 함수 안의 setTimeout 내부에서 callback 함수를 호출하였습니다.

이벤트 리스너에서의 콜백함수

이벤트 리스너에서도 마찬가지로 이벤트리스너 함수 파라미터로 함수를 받습니다. 즉, 이벤트 리스너의 순차 실행을 위해서 파라미터에 콜백함수가 들어갑니다.
예를 들어
1. 클릭 후
2. 원하는 함수(파라미터) 실행

const button = document.querySelector(".button");

button.addEventListener("click", function() {
  // 콜백함수
  console.log("버튼 클릭 후 실행해주세요!");
});

버튼이 클릭되기도 전에 함수가 실행되면 안되기에 이벤트리스너에서 콜백함수를 사용하는 것입니다.


콜백 지옥! (Call back Hell)

function hell(callback) {
  setTimeout(() => {
    callback();
  }, 1000);
}
hell(() => {
  console.log('1');
  hell(() => {
    console.log('2');
    hell(() => {
      console.log('3');
    })
  })
})

hell이라는 함수를 사용해 1초마다 1, 2, 3 하나씩 출력하도록 코드를 작성했습니다.
물론 현재 코드 길이는 짧아서 1, 2, 3 을 순차적으로 찍겠구나 라고 생각할지모르지만.
위 코드는 정말 가독성이 좋지 않은 코드라고 확 느껴질 것이다.

hell(() => {
  console.log('1');
  hell(() => {
    hell(() => {
      console.log('3');
    })
    console.log('2');
  })
})

이렇게 작성해도 동일한 결과가 나타납니다. (가독성이 좋지 않고 이해하기도 어렵습니다.)
이러한 현상을 callback hell 이라 한다.
콜백함수를 잘 사용하면 비동기 처리를 할 수 있어 순차적인 실행이 필요할 떄 유용하게 잘 사용할 수 있지만, 콜백 지옥을 경험할 수도 있다는 점을 유의하자!

콜백지옥을 해결하는 방안으로 promise / async await을 사용하는 방법이 존재한다.


콜백함수 사용 원칙

1. 익명 함수 사용

let number = [1, 2, 3, 4, 5]

number.forEach(function(x) {
  console.log(x * 2);
});

callback 함수는 이름이 없는 '익명 함수' 사용
함수 내부에서 실행되기 때문에 이름을 붙이지 않아도 됩니다.

2. 함수의 이름만 넘기기

function whoAreYou(name, callback) {
  console.log('name: ', name);
  callback();
}

function endFunc() {
  console.log("End Function");
}

whoAreYou('yongmin', endFunc);

// name: yongmin
// End Function
  

JS에서는 null과 undefined 타입을 제외하고 모든 것을 객체로 다룹니다.
함수를 변수 or 다른 함수의 변수처럼 사용이 가능합니다.
함수를 콜백함수로 사용할 경우, 함수의 이름(endFunc)만 인자로 넘겨주면 됩니다.
즉, endFunc() 이렇게 해서 인자로 넘겨줄 필요가 없습니다.

3. 전역변수, 지역변수를 콜백함수의 파라미터로 전달.

let animal = 'puppy'; // Global Parameter

function callbackFunc(callback) {
  let snack = 'dog chew'; // Local variable
  callback(snack)
}

function play(snack) {
  console.log(`animal: ${animal} / snack: ${snack}`);
}

callbackFunc(play);

// animal: puppy / snack: dog chew

콜백함수 사용시 주의할 점

this를 사용한 callback 함수

콜백함수도 함수이기 때문에,
어떤 객체의 메서드로 있던 함수가 인자로 전달될 경우에는 객체의 메서드가 아닌 함수자체로 전달됩니다.

콜백함수 내부에서 this는 해당 콜백함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역 객체를 참조합니다.

let obj = {
  array: [1, 2, 3],
  foo: function(a, b) {
    console.log(this, a, b);
  }
}

obj.foo(4, 5); 
[10, 20, 30].forEach(obj.foo);

(1) obj.foo(4, 5)
. 앞에 obj 라는 객체가 있으니 foo 함수는 obj의 메서드 foo의 자격으로 호출이 됐습니다.
따라서 this는 .앞에 있는 obj를 가리키게 되고,
콘솔에는 {array: Array(3) , foo: f} 4 5)가 출력됩니다.

(2) [10, 20, 30].forEach(obj.foo);
obj.foo 가 통쨰로 forEach 함수의 인자로 들어가 있스빈다.
생긴건 . 이 붙은 메서드의 모양이지만, 메서드를 그대로 준 것이 아닌 그냥 콜백함수의 역할을 하는 함수로서 있는 것 뿐입니다.

[10, 20, 30].forEach(function(a, b) {
  console.log(this, a, b);
});

단순히 obj.foo가 나타내는 function(a, b) {console.log(this, a, b);} 가 들어간 것에 불과합니다. 이렇게 콜백함수로 들어가 있을 경우에는 . 앞의 obj와 직접적인 연관성이 없습니다.

별도로 this를 설정하지 않았으므로 이때의 this는 전역객체인 window를 바라봅니다.

위와 같이 출력됨을 확인할 수 있습니다.

즉, 객체의 소속이었던 메서드를 콜백함수로써 (전달인자로) 전달하면, 그 콜백함수 내부에서 this는 더이상 자신이 속해있던 객체를 바라보지 않습니다.


binding 종류

1. default

기본적으로 this는 전역객체를 가리킵니다. node환경에서는 global, 브라우저에서는 window 객체를 가리킵니다.

function foo() {
  const a = 10;
  console.log(this.a);
}

foo(); // undefined

위의 코드에서의 this는 전역 객체에 binding되고 전역 객체에는 a라는 property가 없기 떄문에 undefined가 출력됩니다. 출력값을 원할하게 받고 싶으면 전역객체에 a라는 프로퍼티를 만들어주어야 합니다.

window.a = 10;

function foo() {
  console.log(this.a);
}

foo(); //10

하지만 'use strict'모드에서는 기본 binding 대상에서 전역 객체는 제외됩니다. 전역 객체를 참조해야할 this가 있다면 그 값은 undefined가 됩니다.

'use strict'
window.a = 10;

function foo() {
  console.log(this.a);
}

foo();

/*
VM169:5 Uncaught TypeError: Cannot read properties of undefined (reading 'a')
    at foo (<anonymous>:5:20)
    at <anonymous>:8:1
*/

2. 암시적 바인딩 (Implicit Binding)

window.a = 'window';

function foo() {
  console.log(this.a);
}

const obj = {
  a: 1,
  foo: foo
}

obj.foo() // 1
foo() // window

함수가 호출되는 위치에서 this가 바인딩됩니다.
위의 경우 this가 { a:1, foo: foo} 객체에 바인딩 되면서 a를 가져옵니다.
만약 foo()를 호출한다면 this는 기본 바인딩이 되면서 a를 알 수 없으므로 'window'가 출력됩니다.

function foo() {
  console.log(this.a);
}

const obj = {
  a: 1,
  foo: foo
}

// reference case 1
const foo2 = obj.foo;
foo2(); // undefined 함수 호출 위치에 따라서 런타임 바인딩이 되 원하는 1이 나오지 않음.

// calback case 2
setTimeout { obj.foo, 100 }; // undefined

/*
let f = obj.foo;
setTimeout(f, 100); // 즉 obj 컨텍스트를 잃어버립니다.

명시적 바인딩

  1. call 함수 사용
function foo() {
  console.log(this.a);
}

const obj = { a: 1 };

foo.call(obj); // 1
  1. this / apply 함수 사용
function add(a, b) {
  return a + b;
}

add.call(null, 1, 2); // 3
add.apply(null, [1, 2]); // 3

두 함수 모두 첫번쨰 인자에는 this로 사용할 (this가 가리킬) 객체를 넣어줍니다. 이떄 null이나 undefined를 넣으면 자동적으로 this는 global객체를 가리킵니다. call은 함수에 사용될 인자들을 , 로 구분지어 넣고, apply는 인자들을 배열 [] 로 넣어줍니다.

  1. bind 사용
function foo() {
  console.log(this.a);
}

const obj = {
  a: 1,
  foo: foo
}

const foo2 = obj.foo.bind(obj);
const foo3 = foo2.bind({a: 4});

foo2(); // 1
foo3(); // 1

call과 apply의 경우 함수의 실행값을 반환하는 반면 bind는 this가 새롭게 바인딩한 함수를 반환합니다. bind를 통해 생성된 함수의 this는 최초의 바인딩된 객체로 고정됩니다.
다시 명시적으로 바인딩해도 this는 변경되지 않습니다.

const globalThis = this;
const arrowThis = () => this;

arrowThis() == globalThis // true

화살표 함수에는 this가 없습니다. 대신 화살표 함수를 둘러싸는 렉시컬 범위(lexical scope)의 this가 사용됩니다. 바깥 범위에서 this를 찾는다. 따라서 예시에서 this는 바로 상위인 글로벌을 가리킵니다.

  1. new를 통한 바인딩
function Foo() {
  console.log(this);
}

new Foo(); // Foo

new를 통해 생성하게되면 this는 새로 생성된 객체를 가리킵니다.

profile
현재 블로그를 이전했습니다! (https://www.yolog.co.kr)

0개의 댓글