this 와 콜백 함수

jj·4일 전

this

1. this는 실행 컨텍스트가 생성되는 순간(함수가 호출되는 순간)에 결정된다.

this는 지금 이 함수를 호출할 때, 누구를 기준으로 실행하느냐를 가리키는 값이다.
this는 실행 컨텍스트가 생성되는 순간 바인딩(동적 바인딩)을 하기 때문에 어떤 식으로 호출하느냐에 따라 가리키는 값이 달라진다.

2. 전역 객체의 this: 전역 객체(window, global)

3. 함수/메서드의 this

함수/메서드 앞에 객체가 있으면 해당 객체가 this의 대상이 된다.
없으면 전역 객체가 대상이 된다.

const dog = {
  name: "강아지",
  food: function() {
    console.log(this.name + "가 밥을 먹습니다.");
  }
};

dog.food();
// 이 경우에 this는 dog

const eat = dog.food;
eat();
// 이 경우에 this는 전역 객체
//(전역 객체에 name이 없으므로 undefined가 밥을 먹습니다. 를 출력)

4. 콜백 함수의 this

콜백 함수를 넘겨받은 대상이 어떻게 호출하느냐에 따라서 this의 대상이 달라진다.
기본적으로는 함수의 this와 같지만 제어권을 가진 함수가 콜백의 this를 지정해둔 경우가 있다. 이런 경우에도 this를 바인딩해서 콜백을 넘기면 그것에 따른다.

5. 생성자 함수의 this

function Character(name, weapon) {
  this.name = name;
  this.weapon = weapon;
  this.attack = function() {
    console.log(this.name + "가 " + this.weapon + "을(를) 휘두릅니다!");
  };
}

const chiikawa = new Character("치이카와", "토벌봉");
const hachiware = new Character("하치와레", "파란 사스마타");

chiikawa.attack(); // "치이카와가 토벌봉을(를) 휘두릅니다!"
hachiware.attack(); // "하치와레가 파란 사스마타을(를) 휘두릅니다!"
// 이 경우 생성자 함수의 this는 생성된 인스턴스를 따른다.

6.call, apply, bind: this의 명시적 지정방법

var a = function (x, y, z) {
  console.log(this);
  console.log(x, y, z);
};

var b = {
  name: 'bbb'
};

a(1, 2, 3); // 점(.) 없이 불렀으니 this는 전역 객체(window)

a.call(b, 1, 2, 3); // b를 this로 쓰고 x, y, z는 각자 1, 2, 3을 넣음

a.apply(b, [1, 2, 3]); // b를 this로 쓰고 x, y, z는 각자 1, 2, 3을 넣음.
// 배열로 넣어도 알아서 1, 2, 3으로 나눠 가진다. apply의 두 번째 인자는 반드시 배열이여야 한다.

var c = a.bind(b); // bind는 즉시 실행하지 않고 this가 b로 고정된 새로운 함수를 반환한다.
c(1, 2, 3); // 그래서 c(1, 2, 3)을 나중에 호출해도 내부의 this는 b이다.

var d = a.bind(b, 1, 2); // 위와 동일. this가 b로 고정된 새로운 함수를 반환한다.
d(3);

7.ES6 이후 쓰는 방법 (화살표 함수): this를 자체적으로 갖지 않는다

const chiikawa = {
  name: "치이카와",
  eat: function() {
    const newDessert = () => {
      // 2. 화살표 함수는 점(.) 유무를 무시하고, 스코프 체인상 가장 가까운 this를 가져온다.
      console.log(this.name + "가 푸딩을 먹습니다. (화살표 함수)");
    }
    newDessert();
    // 3. 점(.) 없이 단독 호출했지만 this는 여전히 치이카와 (치이카와가 푸딩을 먹습니다.)
  }
};

chiikawa.eat(); // 1. 여기서 eat의 this는 chiikawa로 정해짐.

콜백 함수

1. 콜백 함수란

콜백 함수는 다른 함수에 인자로 전달되어 실행 시점, 매개변수, this 바인딩 등에 대한 제어권을 다른 함수에 위임한 함수이다.
제어권을 위임받은 함수는 실행할 특정 조건이 되면 콜백 함수를 실행하고 그 후 나에게 알려준다.

제어권누가 정하나대표 예시
호출 시점콜백을 받은 함수setInterval
인자(값과 순서)콜백을 받은 함수forEach, map
this콜백을 받은 함수addEventListener, map의 두 번째 인자

2. 호출 시점의 제어권: setInterval

let count = 0;
const timer = setInterval(() => {
  console.log(count);
  count++;
  if (count > 10) clearInterval(timer);
}, 3000);

첫 번째 인자(콜백)를 언제 실행할지는 setInterval이 정한다. 여기서는 3초마다 실행한다. 개발자는 "3초마다 실행해 줘"라고 맡기기만 하고, 반복 타이밍은 직접 관리하지 않는다.

🔍 "이 콜백은 어디서 실행되는데?"
시간이 되면 태스크 큐에 들어가고, 이벤트 루프가 콜 스택이 빈 것을 확인한 뒤 콜 스택으로 넘겨서 실행한다.

3. 인자의 제어권: forEach, map

const result = [10, 20, 30].map((value, index, array) => value * 2);
console.log(result); // [20, 40, 60]

map은 배열의 요소를 하나씩 꺼내 콜백을 반복 호출하고, 반환값을 모아 새 배열을 만든다. 이때 콜백에 넘겨주는 값과 순서는 map이 정해둔 규칙이다.

첫 번째 인자: 현재 값
두 번째 인자: 현재 값의 인덱스
세 번째 인자: 배열 자체

🔍 "만약에 인자 이름을 바꾸거나 갯수를 안 맞추면 어떻게 돼?"

[10, 20, 30].map((index, value) => value);

이름만 바꾼 것이지 위치는 그대로다. 첫 번째 인자(실제 값)가 index라는 이름에 담긴다. 컴퓨터는 이름이 아니라 몇 번째 인자인가로만 구분한다. 그래서 콜백은 받는 쪽이 정한 규칙에 맞춰 작성해야 한다.
또 인자 갯수를 맞추지 않더라도 콜백 함수 내에서 필요한 인자가 아니라면 쓰지 않아도 제대로 작동한다. 반드시 value, index, array의 세 가지 인자를 넘겨줄 필요는 없다. 다만 아래와 같이 순서는 지켜야 한다.

// 값만 쓸 때 → 뒤는 생략 OK
[10, 20, 30].map((value) => value * 2);
// 인덱스만 쓸 때 → 앞자리는 채워야 함
[10, 20, 30].map((_, index) => index);
// 아무것도 안 쓸 때 → 전부 생략 OK
[10, 20, 30].map(() => 'a');

4. this의 제어권

콜백 함수도 결국 함수이므로 별도로 지정이 없으면 this는 전역 객체를 가리킨다. 하지만 콜백을 호출하는 쪽이 this를 따로 지정해 주면 그 대상을 가리킨다.

  1. forEach, map의 두 번째 인자로 지정
const obj = { multiplier: 2 };

const r = [1, 2, 3].map(function (x) {
  return x * this.multiplier;   // this = obj
}, obj);                        // 두 번째 인자 = this로 쓸 대상

console.log(r); // [2, 4, 6]

map을 직접 구현한다면 thisArg가 있으면 그 값을, 없으면 전역 객체를 this로 지정하는 식이 된다.

  1. addEventListener는 this를 이벤트 대상 요소로 정해 둔다
button.addEventListener('click', function (e) {
  console.log(this);  // button 요소
  console.log(e);     // 이벤트 객체
});

인자(이벤트 객체)와 this(DOM 요소) 모두 addEventListener가 정해서 넘겨주는 것이다. map과 달리 기본값이 고정되어 있다.

5. 콜백으로 넘긴 메서드는 "메서드"가 아니라 "함수"다

const obj = {
  label: 'obj',
  logValues(v, i) {
    console.log(this.label, v, i);
  }
};

obj.logValues(1, 2); // 'obj' 1 2
[4, 5, 6].forEach(obj.logValues);
// undefined 4 0
// undefined 5 1
// undefined 6 2

obj.logValues(1, 2)는 메서드 이름 앞에 점이 있으니 메서드로서 호출된다. 그래서 this가 obj다.

forEach(obj.logValues)는 obj를 this로 하는 메서드를 통째로 넘긴 게 아니다. obj.logValues가 가리키는 함수만 넘긴 것이다. obj와의 연결이 끊겼고, this를 따로 지정하지 않았으니 전역 객체를 바라본다.

콜백 안의 this를 원하는 객체로 고정하기

// 1. forEach의 두 번째 인자로 this 지정
[4, 5, 6].forEach(obj.logValues, obj);

// 2. bind로 this를 고정한 새 함수 만들기
[4, 5, 6].forEach(obj.logValues.bind(obj));

둘 다 obj 4 0, obj 5 1, obj 6 2로 정상 출력된다.

6. 비동기와 콜백 지옥

동기: 현재 코드가 끝나야 다음 코드를 실행한다.
비동기: 현재 코드의 완료와 무관하게 바로 다음 코드로 넘어간다. (setTimeout 등)

🔍 "콜백 지옥?"
비동기 작업을 콜백 함수로 반복해서 처리하다 보면 콜백 안에 콜백을 계속 넣게 되는데, 들여쓰기가 감당 안 될 만큼 깊어지는 현상이다.

setTimeout(() => {
  console.log('실행 1');
  setTimeout(() => {
    console.log('실행 2');
    setTimeout(() => {
      console.log('실행 3');
    }, 500);
  }, 500);
}, 500);


try {
  setTimeout(() => { throw new Error('Error!'); }, 1000);
} catch (e) {
  // 에러를 캐치하지 못한다
  console.error('캐치한 에러', e);
}

들여쓰기가 깊어져 읽기도 수정하기도 어렵고, 에러 처리가 어렵다. try 블록은 이미 종료되어 스택에서 내려갔는데 뒤늦게 콜 스택에 올라온 비동기 작업에서 에러가 발생하게 되면 try 블록이 에러를 잡지 못하게 된다.

해결 방법

✅ 1. Promise (ES6)
비동기 작업이 끝나면 resolve(성공) 또는 reject(실패)를 호출한다. 둘 중 하나가 호출되기 전에는 then/catch로 넘어가지 않으므로, 비동기를 순서대로 이어 붙인 것처럼 쓸 수 있다.

const wait = (message) =>
  new Promise((resolve) => {
    setTimeout(() => {
      console.log(message);
      resolve(); // 끝났다고 알려야 다음 then으로 넘어간다
    }, 500);
  });

wait('실행 1')
  .then(() => wait('실행 2'))
  .then(() => wait('실행 3'));

반복되는 부분을 wait 함수로 묶은 것도 클로저(message를 기억)를 활용한 방식이다.

✅ 2. async/await (ES2017)
가장 많이 쓰는 방식이다. 함수 앞에 async, 비동기 작업 앞에 await를 붙이면 뒤쪽을 Promise로 자동 전환하고, resolve된 뒤에야 다음 줄로 진행한다. Promise를 좀 더 사용하고 읽기 쉽게 만든 것이다.

async function setTime() {
  await wait('실행 1');
  await wait('실행 2');
  await wait('실행 3');
}
setTime();

위의 콜백 지옥과 같은 동작인데, 동기 코드처럼 위에서 아래로 읽힌다.

✅ 3. Generator (ES6)
async/await가 나오기 전에 사용하던 문법. 지금은 "이런 게 있다" 정도만 알아도 된다.

7.한 줄 정리

개념핵심
콜백 함수다른 함수에 인자로 넘겨서 제어권을 위임한 함수
호출 시점받은 쪽이 판단해서 실행 (setInterval)
인자받은 쪽이 정한 값과 순서대로 전달 (map, forEach)
this기본은 전역 객체, 받은 쪽이 지정하면 그 대상, bind로 고정 가능
메서드 vs 콜백메서드를 콜백으로 넘기면 객체와의 연결이 끊겨 함수로 호출되어 this가 달라진다.
콜백 지옥중첩 콜백으로 읽기 어려워지는 현상, Promise / async-await로 해결
profile
frontend

0개의 댓글