[JS] 콜백함수 제어권

jiny·2025년 10월 26일

기술 면접

목록 보기
70/78

🗣️ 어떤 항목에서 콜백함수를 전달받은 함수에게 제어권이 이양되나요?

  • 의도: 지원자가 콜백 함수의 제어권 이양 과정을 이해하고 있는지 평가
    • 콜백 함수를 사용하는 대표적인 상황(이벤트 핸들링, 타이머, AJAX 요청 등)에 대해 설명한다.
    • 콜백 함수를 사용하는 예제를 떠올려본다.
  • 모범 답안

    자바스크립트에서 콜백 함수함수의 제어권을 다른 함수에게 넘겨주는 방식을 의미합니다.
    즉, 함수를 직접 호출하는 대신 다른 함수의 인자로 전달하여, 그 함수가 언제, 어떤 인자로, 몇 번 호출할지를 결정하도록 위임하는 것입니다.

    예를 들어 forEach, setTimeout, addEventListener 같은 내장 함수들은 모두 콜백 함수를 인자로 받아 내부에서 실행 시점을 제어합니다.
    이때 호출의 흐름, 즉 제어권은 호출자가 아닌 forEach, setTimeout과 같이 해당 함수에게 넘어가며, 그 함수가 조건에 맞을 때 콜백을 실행합니다.

    즉, "제어권이 이양된다"는 말은 함수 호출의 주체와 실행 시점을 내가 아닌, 콜백을 전달받은 함수가 결정한다는 의미입니다.


📝 개념 정리

🌟 콜백 함수란?

  • 다른 함수의 인자로 전달되어, 그 함수의 내부에서 특정 시점에 실행되는 함수
  • 내가 직접 callback()을 호출하는 것이 아니라, 다른 함수가 대신 호출하는 함수
function greeting(name) {
  console.log(`Hello, ${name}`);
}

function processUserInput(callback) {
  const name = "현진";
  callback(name); // 여기서 greeting이 호출됨
}

processUserInput(greeting); // 콜백 전달

위 예제에서 processUserInput은 콜백(greeting)을 전달받아 실행 시점을 스스로 결정한다.
➡️ 이때 제어권(Control)processUserInput 함수로 넘어간다.


🌟 제어권(Control)의 개념

"제어권이 이양된다"는 말은 다음을 의미한다.

함수 호출의 시점, 횟수, 전달되는 인자 등을 호출자가 아닌 콜백을 받은 함수가 결정한다는 것

즉,

  • 내가 언제 콜백을 실행할지 결정하지 않고
  • 제어 흐름의 주도권을 상대 함수에 위임하는 것이다.

이 개념은 자바스크립트의 비동기 처리고차 함수(Higher-Order Function)에서 핵심적인 동작 원리이다.

🔎 고차 함수란?

  • 함수를 인자로 받거나, 함수를 반환(혹은 둘 다)하는 함수
  • 전제: 자바스크립트에서 함수는 일급 객체이므로 값처럼 전달·반환할 수 있다.

🔎 고차 함수와 제어권 이양과의 연결

  • 콜백을 인자로 받는 순간, 콜백의 실행 시점/횟수/인자를 받은 함수가 통제한다. → 제어권 이양
  • 이로써 동작(언제/어떻게 실행)은 감추고, 행위(무엇을 할지)만 바깥에서 주입추상화·재사용성·테스트 용이성 상승

🔎 고차 함수 대표 예시 (JS 표준 API 중심)

  • 배열 고차 함수
    [1, 2, 3].map(x => x * 2); // 콜백의 반환값으로 새 배열 생성
    [1, 2, 3].filter(x => x % 2); // 콜백의 진리값으로 요소 선별
    [1, 2, 3].reduce((acc, x) => acc + x, 0); // 누적 계산
    [1, 2, 3].forEach(x => console.log(x)); // 부수효과
    ➡️ 모두 함수를 인자로 받는 고차 함수로, 콜백 호출 방식은 메서드가 결정함
  • 비동기/이벤트 영역
    setTimeout(() => { /* ... */ }, 0); // 타이머 시스템이 시점 결정
    btn.addEventListener('click', (e) => { /* ... */ }); // 브라우저 이벤트 시스템이 호출
    Promise.resolve(1).then(v => v + 1); // 엔진이 마이크로태스크로 스케줄
    ➡️ 콜백 인자를 받아 비동기 시점에 호출함 → 고차 함수 패턴
  • 함수를 반환하는 고차 함수
    const withLog = fn => (...args) => {
      console.time('t');
      try { return fn(...args); }
      finally { console.timeEnd('t'); }
    };
    const sum = (a, b) => a + b;
    const loggedSum = withLog(sum);
    loggedSum(1, 2);
    ➡️ withLog함수를 받아 새 함수를 반환

🌟 제어권 이양의 의미를 코드로 비교

  • 제어권이 나에게 있을 때
    function run(callback) {
      callback(); // 내가 직접 호출함
    }
    run(() => console.log("내가 직접 제어하는 호출"));
    ➡️ 콜백을 즉시 실행하므로, 제어권이 이양되지 않음
  • 제어권이 이양될 때
    setTimeout(() => console.log("제어권이 타이머에 있음"), 2000);
    ➡️ setTimeout이 콜백의 실행 시점(2초 후)을 결정하므로, 제어권이 setTimeout에게 이양됨

🌟 제어권 이양이 일어나는 대표 사례

1. 배열 메서드: forEach, map, filter

⭐ 제어권 주체: 각 메서드의 내부 구현
⭐ 핵심: "언제/몇 번/무슨 인자로 콜백을 부를지"를 메서드가 결정

  • 공통 규칙

    항목내용
    호출 횟수배열의 "실재하는 인덱스"마다 한 번
    희소 배열의 구멍(hole)은 건너뜀
    호출 시점메서드 실행 중 동기적으로 즉시 (비동기 아님)
    전달 인자(요소값, 인덱스, 원본배열)의 3개를 메서드가 고정 규약으로 넣어줌
    this 바인딩두 번째 인자 thisArg콜백의 this를 지정할 수 있음
    중간 변경 영향순회 중 배열 길이/값을 바꾸면 명세에 따라 그 이후 호출에 영향
    (예: 뒤에 추가된 요소는 일반적으로 순회 대상 아님)
    중단 불가forEachbreak/return으로 중단 불가
    필요 시 some/every 사용을 고려

    💡 희소 배열(Sparse Array)란?

    • 배열의 인덱스가 연속적이지 않은 배열
    • 배열의 길이는 크지만 중간 인덱스에 실제 값이 존재하지 않는 상태
      • 이렇게 값이 비어 있는 인덱스구멍(hole)이라고 부른다.
    const arr = [10, , 30];
    console.log(arr.length); // 3

    이 배열의 인덱스는 다음과 같다.

    인덱스상태
    010존재
    1❌ (없음)구멍(hole)
    230존재

    ➡️ 즉, 길이는 3이지만 실제로는 값이 2개만 있는 배열이다.

    💡 구멍(hole)이란?

    • 배열 내부에서 값이 전혀 할당되지 않은 인덱스 공간을 의미한다.

    • undefined랑은 다르다.

      const a = [undefined]; // 요소 1개 존재, 값이 undefined
      const b = [,]; // 요소 자체가 없음 (hole)
      console.log(0 in a); // true -> 인덱스 0 존재
      console.log(0 in b); // false -> 인덱스 0 비어있음

      ➡️ 즉, undefined존재하지만 값이 없는 것이고, hole존재조차 하지 않는 것이다.

    💡 "희소 배열의 구멍을 건너뛴다"는 말의 의미

    배열 메서드(forEach, map, filter, some, every, reduce) 중 일부는 실제로 존재하는 인덱스에만 콜백을 실행한다.

    즉, 구멍(hole)이 있으면 그 인덱스는 아예 무시하고 지나

  • 메서드 차이
    • forEach
      • 반환값 없음(항상 undefined)
      • 부수효과 중심
    • map
      • 콜백의 반환값으로 새 배열 생성
      • 길이는 원본과 동일 (구멍은 유지 규칙에 따름)
    • filter
      • 콜백 진리값에 따라 통과 요소만 모아 새 배열 생성
      • 호출 자체는 모든 존재 인덱스에 대해 1회씩

2. 타이머 함수: setTimeout, setInterval

⭐ 제어권 주체: 브라우저(Web API) / Node 타이머 서브시스템
⭐ 핵심: 시간 경과를 관찰하고, 만기 시 태스크 큐에 콜백을 예약하는 쪽이 타이머 시스템

  • setTimeout(fn, delay)
    • 보장 사항
      • delay 밀리초 이후 최소 지연 뒤에 실행 기회 제공
      • 이벤트 루프 대기, 탭 비활성화 스로틀 등으로 더 늦어질 수 있음
    • 실행 과정: 타이머 만기 → 태스크 큐에 콜백 투입콜 스택이 비면 이벤트 루프가 꺼내 실행
    • 인자 전달: setTimeout(fn, d, a, b...)처럼 추가 인자를 콜백에 전달 가능(브라우저/Node 지원)
  • setInterval(fn, interval)
    • 주기적 예약: 매 주기마다 예약만 보장
    • 백투백 방지/스로틀: 탭 비활성·백그라운드에서 최소 지연 상향

3. 이벤트 핸들러: addEventListener

⭐ 제어권 주체: 브라우저의 이벤트 시스템 (이벤트 루프 + DOM 이벤트 흐름)
⭐ 핵심: 실제 사용자/DOM 이벤트가 발생할 때 브라우저가 콜백 호출 여부/순서/인자(Event 객체)를 결정

  • 규칙

    항목내용
    호출 조건/시점해당 타입 이벤트 발생 시(클릭, 입력, 네트워크, 커스텁) 비동기적으로 큐에 전달되어 콜백 실행
    전달 인자첫 번째 인자로 이벤트 객체(MouseEvent, KeyboardEvent 등)를 브라우저가 생성/전달
    캡처링/버블링useCapture 또는 options.capture에 따라 이벤트 흐름 단계에서 호출되는지 결정
    리스너 다중 등록/순서같은 노드·같은 단계에서는 등록 순서대로 호출(명세 규약)

    💡 브라우저의 이벤트 시스템이란?

    • 사용자의 행동(click, input, scroll 등)이나 브라우저 내부의 상태 변화(load, error, network 등)를 감지하고,
      이에 따라 등록된 콜백(이벤트 핸들러)을 실행해주는 메커니즘 전체
    • 브라우저 안에서 이벤트가 감지되고 → 전달되고 → 콜백 실행까지 이뤄지는 전체 자동 흐름

    💡 브라우저의 이벤트 시스템 구성 요소

    • 이벤트 루프(Event Loop): "언제 콜백을 실행할지"를 결정하는 브라우저의 비동기 처리 엔진
    • DOM 이벤트 흐름(Event Flow): "어떤 순서로 어떤 노드에게 이벤트를 전달할지"를 결정하는 DOM 구조 기반의 전달 규칙

    ➡️ 즉, 이벤트 루프시간의 제어("언제 실행할까?"), DOM 이벤트 흐름공간의 제어("누가 받을까?")를 담당함

    💡 전체 이벤트 처리 흐름 예시

    button.addEventListener("click", () => {
      console.log("Clicked!");
    });
    1. 사용자가 마우스를 클릭 → OS에서 브라우저로 "click" 신호 전달
    2. 브라우저의 이벤트 시스템click 이벤트 발생을 감지
    3. 해당 이벤트가 브라우저의 이벤트 큐(event queue)에 등록
    4. 이벤트 루프(Event Loop)가 현재 실행 중인 콜스택이 비면, 큐에 있는 이벤트를 꺼내 실행
    5. DOM 트리 상에서 캡처링 → 타깃 → 버블링 순서로 이벤트 전달
    6. addEventListener로 등록된 콜백해당 단계(캡처링 or 버블링)에 맞게 호출

    💡 DOM 이벤트 흐름

    HTML 문서는 트리 구조로 되어 있는데, 이벤트가 발생하면 그 트리를 따라 이동한다.

    1. 캡처링 단계 (Capturing Phase)
      • 최상위(windowdocumenthtmlbody → ...)에서 이벤트 대상 요소까지 내려가며 이벤트를 전달함
      • addEventListener("click", handler, { capture: true })로 감지 가능
    2. 타깃 단계 (Target Phase)
      • 실제 이벤트가 발생한 요소(예: <button>)에 도달
    3. 버블링 단계 (Bubbling Phase)
      • 다시 위쪽으로 거슬러 올라가며 부모 요소들에게 이벤트를 전달함
      • 기본값({ capture: false })일 때는 이 단계에서 실행됨

    💡 제어권 이양과의 연결

    button.addEventListener("click", callback);
    • 여기서 callback은 코드 작성자가 직접 호출하지 않음
    • 클릭 시점, 호출 순서, 인자(Event 객체)브라우저의 이벤트 시스템이 전적으로 관리

    ➡️ 즉, 언제, 어떤 인자(event 객체)로 콜백을 실행할지 결정하는 권한이 브라우저의 이벤트 시스템에게 이양된 것임

4. 비동기 처리: fetch, Promise.then, async/await

⭐ 제어권 주체: Web API + JS 엔진의 Promise/마이크로태스크 스케줄러
⭐ 핵심: 비동기 작업의 완료/실패 시점과 그에 따른 콜백(then/catch)의 마이크로태스크 실행을 런타임이 결정

  • fetch(url, options)
    • 네트워크 계층(Web API)가 요청을 수행하고 응답 수신 후 Promise를 이행/거부
    • 응답 도착 시점은 네트워크 상태/캐시/리디렉션 등에 따라 런타임이 결정
    • 그 결과로 이어지는 .then(res => ...) 콜백은 마이크로태스크 큐에 스케줄됨(콜스택이 비면 즉시 처리)
  • Promise.then(onFulfilled, onRejected)
    • 상태 변경(fulfilled/rejected)등록된 콜백을 마이크로태스크로 예약
    • 순서 보장: 같은 턴에 resolved된 것들은 등록 순서대로 실행
    • 체이닝 제어: 콜백 반환값이 다음 Promise의 상태를 결정(값 → fulfill, 예외/거부 → reject)
  • async/await
    • 문법 설탕: await는 우변의 Promise가 settle되면 그 이후 코드를 마이크로태스크로 이어서 실행하라는 스케줄링 지시
    • 오류 전파: await 중 거부되면 예외로 던져짐try/catch로 처리(제어 흐름을 런타임이 스케줄)

🌟 제어권 이양의 본질

구분설명
언제 호출할지호출 시점을 전달받은 함수가 결정
몇 번 호출할지반복, 조건, 이벤트 횟수 등은 내부 로직에 따라 다름
무엇을 인자로 줄지전달받은 함수가 콜백에 어떤 데이터를 전달할지도 스스로 결정

예시:

[10, 20, 30].forEach((num, idx) => {
  console.log(num, idx);
});
  • forEach가 언제, 몇 번, 어떤 인자(num, idx)를 넘길지 모두 결정한다.
  • 따라서 제어권이 forEach에게 이양된다.

🌟 제어권 이양이 중요한 이유

  1. 비동기 처리 구조의 핵심

    자바스크립트는 단일 스레드 기반이라,
    비동기 작업(setTimeout, fetch, addEventListener)을 효율적으로 처리하기 위해 콜백 기반의 제어권 이양이 필수적이다.

  2. 추상화와 재사용성 향상

    로직을 외부에서 주입(콜백)받아 실행 시점을 제어함으로써,
    함수의 범용성유연성이 높아진다.

  3. 고차 함수(Higher-Order Function)의 기반

    함수를 인자로 전달하고 실행을 위임하는 개념은
    함수형 프로그래밍과 리액트 렌더링 로직에도 직결된다.

0개의 댓글