bind() 메소드 - this 컨텍스트를 영구적으로 고정하는 방법

React로 클래스 컴포넌트를 처음 만들 때 이런 경험을 한 적이 있나요?

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  increment() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return <button onClick={this.increment}>클릭</button>;
  }
}

버튼을 클릭하면… 에러 발생! 💥

TypeError: Cannot read property 'setState' of undefined

“분명히 this.increment라고 썼는데 왜 thisundefined지?”

저도 처음에는 이 문제로 몇 시간을 헤맸습니다. 그리고 누군가 알려준 해결책:

<button onClick={this.increment.bind(this)}>클릭</button>

신기하게도 작동합니다! 하지만 “왜 bind(this)를 써야 하는가?”에 대한 깊은 이해 없이 그냥 “그렇게 하라니까 하는구나”라고만 생각했습니다.

이 문서에서는 bind()가 무엇인지, 왜 필요한지, 그리고 실제로 어떻게 활용해야 하는지를 처음부터 끝까지 알아보겠습니다.

목차

왜 bind를 이해해야 할까요?

1. this가 사라지는 문제를 해결할 수 있습니다

const person = {
  name: '홍길동',
  greet() {
    console.log(`안녕하세요, ${this.name}입니다.`);
  }
};

person.greet(); // "안녕하세요, 홍길동입니다." ✅

// 메소드를 변수에 할당하면?
const greet = person.greet;
greet(); // "안녕하세요, undefined입니다." ❌

같은 함수인데 호출 방식에 따라 this가 달라집니다! bind()를 이해하면 이 문제를 완벽하게 해결할 수 있습니다.

2. 이벤트 핸들러와 콜백에서 필수입니다

실무에서 가장 많이 마주치는 상황입니다.

class Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    // ❌ this가 사라짐!
    setInterval(function() {
      this.seconds++; // undefined.seconds++
      console.log(this.seconds);
    }, 1000);
  }
}

const timer = new Timer();
timer.start(); // NaN, NaN, NaN...

bind()를 이해하지 못하면 이런 버그와 싸우며 시간을 낭비하게 됩니다.

3. 재사용 가능한 함수를 만들 수 있습니다

bind()의 부분 적용(Partial Application) 기능은 함수형 프로그래밍의 핵심입니다.

function multiply(a, b) {
  return a * b;
}

// 첫 번째 인자를 고정한 새 함수 생성
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

이것은 단순히 “편리한 기능”이 아니라, 재사용 가능한 코드를 만드는 강력한 도구입니다.

먼저, JavaScript의 this를 이해하기

bind()를 이해하려면 JavaScript의 this가 어떻게 작동하는지 먼저 알아야 합니다.

this는 “누가 함수를 호출했는가”로 결정됩니다

다른 언어와 달리, JavaScript의 this함수가 정의된 위치가 아니라 호출된 방식에 따라 달라집니다.

const obj = {
  name: 'Alice',
  greet() {
    console.log(`Hello, ${this.name}`);
  }
};

// 1. 메소드로 호출 → this는 obj
obj.greet(); // "Hello, Alice"

// 2. 일반 함수로 호출 → this는 undefined (strict mode)
const fn = obj.greet;
fn(); // "Hello, undefined"

// 3. 다른 객체에서 호출
const anotherObj = { name: 'Bob' };
anotherObj.greet = obj.greet;
anotherObj.greet(); // "Hello, Bob"

this가 사라지는 4가지 상황

// 상황 1: 메소드를 변수에 할당
const greet = obj.greet;
greet(); // this 손실

// 상황 2: 콜백으로 전달
setTimeout(obj.greet, 1000); // this 손실

// 상황 3: 이벤트 핸들러
button.addEventListener('click', obj.greet); // this는 button

// 상황 4: 배열 메소드
['a', 'b'].forEach(obj.greet); // this 손실

이 모든 상황에서 bind()가 해결책입니다!

bind란 무엇인가?

bind()는 함수의 this 값을 영구적으로 고정한 새로운 함수를 만드는 메소드입니다.

레스토랑 비유로 이해하기

bind()를 레스토랑의 예약 시스템으로 생각해보세요.

일반 함수:
손님: "자리 주세요!"
직원: "어느 테이블에 앉으시겠어요?" (this가 매번 달라짐)

bind된 함수:
손님: "창가 자리로 예약했어요!" (this가 고정됨)
직원: "네, 창가 자리로 안내하겠습니다."

기본 문법

const boundFunction = originalFunction.bind(thisArg, arg1, arg2, ...);

매개변수:

  • thisArg: 바인딩할 this
  • arg1, arg2, ...: (선택) 미리 설정할 인자들

반환값:

  • 지정된 this와 초기 인자들이 설정된 새로운 함수

간단한 예제

const person = {
  name: '홍길동',
  greet() {
    console.log(`안녕하세요, ${this.name}입니다.`);
  }
};

// this를 person으로 고정한 새 함수 생성
const boundGreet = person.greet.bind(person);

// 이제 어떻게 호출해도 this는 person
boundGreet(); // "안녕하세요, 홍길동입니다." ✅

const fn = boundGreet;
fn(); // "안녕하세요, 홍길동입니다." ✅

setTimeout(boundGreet, 1000); // "안녕하세요, 홍길동입니다." ✅

bind의 핵심 기능

1. this 바인딩 - 함수의 컨텍스트 고정

가장 기본적이고 중요한 기능입니다.

class User {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log(`안녕하세요, ${this.name}입니다.`);
  }
}

const user = new User('철수');

// ❌ 문제 상황: this 손실
setTimeout(user.sayHello, 1000); // "안녕하세요, undefined입니다."

// ✅ 해결: bind로 this 고정
setTimeout(user.sayHello.bind(user), 1000); // "안녕하세요, 철수입니다."

시각화:

원본 함수:
┌─────────────┐
│  sayHello   │
│  this = ?   │  ← 호출 시점에 결정
└─────────────┘

bind 적용:
┌─────────────┐
│ boundGreet  │
│ this = user │  ← 영구적으로 고정됨
└─────────────┘

2. 부분 적용 (Partial Application) - 인자 미리 설정

함수의 일부 인자를 미리 설정하여 새로운 함수를 만들 수 있습니다.

function greet(greeting, punctuation, name) {
  return `${greeting}, ${name}${punctuation}`;
}

// 첫 번째 인자만 고정
const sayHello = greet.bind(null, 'Hello');
console.log(sayHello('!', 'Alice')); // "Hello, Alice!"
console.log(sayHello('?', 'Bob'));   // "Hello, Bob?"

// 첫 번째와 두 번째 인자 고정
const sayHelloExcited = greet.bind(null, 'Hello', '!');
console.log(sayHelloExcited('Charlie')); // "Hello, Charlie!"

동작 과정:

원본 함수:
greet(greeting, punctuation, name)
         ↓
sayHello = greet.bind(null, 'Hello')
         ↓
sayHello(punctuation, name)
  → greet('Hello', punctuation, name)

sayHelloExcited = greet.bind(null, 'Hello', '!')
         ↓
sayHelloExcited(name)
  → greet('Hello', '!', name)

3. 새로운 함수 생성 - 원본은 그대로

중요한 점: bind()원본 함수를 수정하지 않고 새로운 함수를 만듭니다.

function showThis() {
  console.log(this.name);
}

const obj1 = { name: 'Object 1' };
const obj2 = { name: 'Object 2' };

const bound1 = showThis.bind(obj1);
const bound2 = showThis.bind(obj2);

// 각각 독립적인 함수
bound1(); // "Object 1"
bound2(); // "Object 2"

// 원본 함수는 그대로
showThis.call(obj1); // "Object 1"
showThis.call(obj2); // "Object 2"

실전 예제로 배우는 bind

예제 1: 이벤트 핸들러에서 this 보존

가장 흔한 사용 사례입니다.

class Counter {
  constructor(elementId) {
    this.count = 0;
    this.element = document.getElementById(elementId);

    // ❌ 나쁜 예: this 손실
    this.element.addEventListener('click', this.increment);
  }

  increment() {
    this.count++; // TypeError: Cannot read property 'count' of undefined
    this.updateDisplay();
  }

  updateDisplay() {
    this.element.textContent = this.count;
  }
}

문제: 이벤트 핸들러 내부에서 this는 이벤트가 발생한 DOM 요소(button)를 가리킵니다.

해결책 1: bind 사용

class Counter {
  constructor(elementId) {
    this.count = 0;
    this.element = document.getElementById(elementId);

    // ✅ 생성자에서 bind
    this.increment = this.increment.bind(this);
    this.element.addEventListener('click', this.increment);
  }

  increment() {
    this.count++; // 이제 정상 작동!
    this.updateDisplay();
  }

  updateDisplay() {
    this.element.textContent = this.count;
  }
}

해결책 2: addEventListener에서 bind

class Counter {
  constructor(elementId) {
    this.count = 0;
    this.element = document.getElementById(elementId);

    // addEventListener에서 직접 bind
    this.element.addEventListener('click', this.increment.bind(this));
  }

  increment() {
    this.count++;
    this.updateDisplay();
  }

  updateDisplay() {
    this.element.textContent = this.count;
  }
}

어느 방법이 더 나을까?

// ✅ 생성자에서 bind (권장)
// 장점: 함수가 한 번만 생성됨
// 단점: 코드가 길어짐
this.increment = this.increment.bind(this);
this.element.addEventListener('click', this.increment);

// ⚠️ addEventListener에서 bind
// 장점: 간결함
// 단점: 이벤트 제거가 어려움 (새 함수가 매번 생성됨)
this.element.addEventListener('click', this.increment.bind(this));
// 이벤트 제거 불가능:
this.element.removeEventListener('click', this.increment.bind(this)); // 작동 안 함!

예제 2: setTimeout/setInterval과 함께 사용

타이머 함수에서도 this가 손실되는 문제가 발생합니다.

class Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    // ❌ 문제: this 손실
    setInterval(function() {
      this.seconds++; // undefined.seconds++
      console.log(this.seconds); // NaN
    }, 1000);
  }
}

const timer = new Timer();
timer.start();

해결책:

class Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    // ✅ bind로 this 고정
    setInterval(function() {
      this.seconds++;
      console.log(`경과 시간: ${this.seconds}초`);
    }.bind(this), 1000);
  }

  // 또는 화살표 함수 (더 현대적)
  startModern() {
    setInterval(() => {
      this.seconds++;
      console.log(`경과 시간: ${this.seconds}초`);
    }, 1000);
  }
}

const timer = new Timer();
timer.start();

예제 3: React 클래스 컴포넌트

React에서 가장 흔하게 bind()를 사용하는 패턴입니다.

class TodoList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      todos: [],
      inputValue: ''
    };

    // ✅ 생성자에서 bind (권장 방법)
    this.addTodo = this.addTodo.bind(this);
    this.handleInputChange = this.handleInputChange.bind(this);
  }

  addTodo() {
    if (this.state.inputValue.trim()) {
      this.setState({
        todos: [...this.state.todos, this.state.inputValue],
        inputValue: ''
      });
    }
  }

  handleInputChange(e) {
    this.setState({ inputValue: e.target.value });
  }

  render() {
    return (
      <div>
        <input
          value={this.state.inputValue}
          onChange={this.handleInputChange}
        />
        <button onClick={this.addTodo}>추가</button>

        <ul>
          {this.state.todos.map((todo, index) => (
            <li key={index}>{todo}</li>
          ))}
        </ul>
      </div>
    );
  }
}

다른 방법들과 비교:

class TodoList extends React.Component {
  constructor(props) {
    super(props);
    this.state = { todos: [] };
  }

  // 방법 1: 생성자에서 bind (위 예제)
  // 장점: 성능 최적화 (함수 한 번만 생성)
  // 단점: 보일러플레이트 코드

  // 방법 2: render에서 bind
  render() {
    return (
      <button onClick={this.addTodo.bind(this)}>추가</button>
    );
    // ❌ 나쁜 예: 렌더링마다 새 함수 생성 (성능 저하)
  }

  // 방법 3: 화살표 함수 (현대적)
  addTodo = () => {
    this.setState({ todos: [...this.state.todos, 'new'] });
  }
  // ✅ 가장 권장: 간결하고 성능도 좋음
}

예제 4: 메소드를 독립 함수로 변환

객체의 메소드를 다른 곳에서 사용해야 할 때 유용합니다.

const calculator = {
  brand: 'SuperCalc',
  add(a, b) {
    console.log(`${this.brand}로 계산: ${a} + ${b} = ${a + b}`);
    return a + b;
  },
  multiply(a, b) {
    console.log(`${this.brand}로 계산: ${a} × ${b} = ${a * b}`);
    return a * b;
  }
};

// ❌ 문제: this 손실
const add = calculator.add;
add(2, 3); // "undefined로 계산: 2 + 3 = 5"

// ✅ 해결: bind로 this 고정
const boundAdd = calculator.add.bind(calculator);
boundAdd(2, 3); // "SuperCalc로 계산: 2 + 3 = 5"

// 배열 메소드와 함께 사용
const numbers = [
  [1, 2],
  [3, 4],
  [5, 6]
];

const results = numbers.map(([a, b]) => boundAdd(a, b));
// "SuperCalc로 계산: 1 + 2 = 3"
// "SuperCalc로 계산: 3 + 4 = 7"
// "SuperCalc로 계산: 5 + 6 = 11"

console.log(results); // [3, 7, 11]

예제 5: 부분 적용으로 재사용 가능한 함수 생성

함수형 프로그래밍의 핵심 패턴입니다.

// 로깅 함수
function log(level, timestamp, message) {
  console.log(`[${level}] ${timestamp}: ${message}`);
}

// 특정 레벨의 로거 생성
const errorLog = log.bind(null, 'ERROR');
const warnLog = log.bind(null, 'WARN');
const infoLog = log.bind(null, 'INFO');

// 타임스탬프도 고정
const now = new Date().toISOString();
const errorLogNow = errorLog.bind(null, now);

// 사용
errorLog('2025-11-20T10:00:00', '데이터베이스 연결 실패');
// [ERROR] 2025-11-20T10:00:00: 데이터베이스 연결 실패

warnLog('2025-11-20T10:01:00', '메모리 사용량 높음');
// [WARN] 2025-11-20T10:01:00: 메모리 사용량 높음

errorLogNow('서버 다운');
// [ERROR] 2025-11-20T10:00:00: 서버 다운

실전 활용:

// 유효성 검사 함수
function isInRange(min, max, value) {
  return value >= min && value <= max;
}

// 특정 범위 검사기 생성
const isAdultAge = isInRange.bind(null, 18, 120);
const isValidPercentage = isInRange.bind(null, 0, 100);
const isValidPort = isInRange.bind(null, 1, 65535);

console.log(isAdultAge(25));           // true
console.log(isAdultAge(15));           // false
console.log(isValidPercentage(50));    // true
console.log(isValidPercentage(150));   // false
console.log(isValidPort(8080));        // true
console.log(isValidPort(70000));       // false

예제 6: 배열 메소드 차용 (Method Borrowing)

배열 메소드를 유사 배열 객체에 사용하는 고급 패턴입니다.

// 유사 배열 객체 (Array-like object)
const arrayLike = {
  0: 'apple',
  1: 'banana',
  2: 'cherry',
  length: 3
};

// ❌ 직접 사용 불가
// arrayLike.map(x => x.toUpperCase()); // TypeError

// ✅ Array 메소드 차용
const map = Array.prototype.map;
const result = map.call(arrayLike, x => x.toUpperCase());
console.log(result); // ['APPLE', 'BANANA', 'CHERRY']

// ✅ bind로 재사용 가능한 함수 생성
const mapArrayLike = map.bind(arrayLike);
const upperCase = mapArrayLike(x => x.toUpperCase());
const lengths = mapArrayLike(x => x.length);

console.log(upperCase); // ['APPLE', 'BANANA', 'CHERRY']
console.log(lengths);   // [5, 6, 6]

arguments 객체와 함께 사용:

function sum() {
  // arguments는 유사 배열 객체
  console.log(arguments); // { 0: 1, 1: 2, 2: 3, length: 3 }

  // ❌ 배열 메소드 사용 불가
  // return arguments.reduce((a, b) => a + b); // TypeError

  // ✅ Array 메소드 차용
  const slice = Array.prototype.slice;
  const args = slice.call(arguments);

  return args.reduce((a, b) => a + b, 0);
}

console.log(sum(1, 2, 3, 4, 5)); // 15

// 또는 bind로 더 간결하게
function sumBind() {
  const toArray = Array.prototype.slice.bind(arguments);
  const args = toArray();
  return args.reduce((a, b) => a + b, 0);
}

// 현대적 방법: spread 연산자
function sumModern(...args) {
  return args.reduce((a, b) => a + b, 0);
}

함정과 주의사항

1. bind는 한 번만 효과적입니다

이미 바인딩된 함수를 다시 바인딩해도 원래의 this는 변경되지 않습니다.

function showThis() {
  return this.name;
}

const obj1 = { name: 'Object 1' };
const obj2 = { name: 'Object 2' };
const obj3 = { name: 'Object 3' };

// 첫 번째 bind
const bound1 = showThis.bind(obj1);
console.log(bound1()); // "Object 1"

// 두 번째 bind (효과 없음!)
const bound2 = bound1.bind(obj2);
console.log(bound2()); // "Object 1" (obj2가 아님!)

// 세 번째 bind (여전히 효과 없음!)
const bound3 = bound2.bind(obj3);
console.log(bound3()); // "Object 1"

이유: bind()는 내부적으로 원본 함수에 대한 참조를 유지하며, 한 번 바인딩되면 변경할 수 없습니다.

시각화:

원본 함수
    ↓
bound1 = showThis.bind(obj1)
    ↓ this = obj1 (고정됨)
    ↓
bound2 = bound1.bind(obj2)
    ↓ this는 여전히 obj1
    ↓
bound3 = bound2.bind(obj3)
    ↓ this는 여전히 obj1

2. 화살표 함수는 bind할 수 없습니다

화살표 함수는 자신의 this를 가지지 않으므로 bind()가 효과가 없습니다.

const arrowFunc = () => {
  console.log(this);
};

const obj = { name: 'test' };

// bind 시도
const boundArrow = arrowFunc.bind(obj);

console.log(arrowFunc === boundArrow); // false (새 함수는 생성됨)
boundArrow(); // 하지만 this는 여전히 렉시컬 스코프의 this

이유: 화살표 함수의 this정의될 때 결정되며, 호출 방식과 무관합니다.

const obj = {
  name: 'Object',

  // 일반 함수: this가 호출 시점에 결정됨
  regularFunc: function() {
    console.log(this.name);
  },

  // 화살표 함수: this가 정의 시점에 결정됨
  arrowFunc: () => {
    console.log(this.name); // 외부 스코프의 this
  }
};

obj.regularFunc(); // "Object"
obj.arrowFunc();   // undefined (전역 객체의 name)

// bind 시도
const anotherObj = { name: 'Another' };

const boundRegular = obj.regularFunc.bind(anotherObj);
const boundArrow = obj.arrowFunc.bind(anotherObj);

boundRegular(); // "Another" (bind 효과 있음)
boundArrow();   // undefined (bind 효과 없음)

3. new 연산자와 함께 사용 시 주의

바인딩된 함수를 생성자로 사용하면 바인딩된 this는 무시됩니다.

function Point(x, y) {
  this.x = x;
  this.y = y;
}

const fixedObj = { x: 100, y: 200 };

// this를 fixedObj로 바인딩
const BoundPoint = Point.bind(fixedObj, 0);

// 생성자로 사용
const point = new BoundPoint(5);

console.log(point.x);  // 0 (첫 번째 인자는 바인딩됨)
console.log(point.y);  // 5
console.log(point);    // Point { x: 0, y: 5 }

// fixedObj는 변경되지 않음
console.log(fixedObj); // { x: 100, y: 200 }

// instanceof 확인
console.log(point instanceof Point); // true

핵심:

  • new와 함께 사용하면 바인딩된 this는 무시됨
  • 대신 새로 생성된 인스턴스가 this가 됨
  • 부분 적용된 인자는 여전히 유효함

4. 함수 속성의 변경

bind()는 새 함수를 만들며, 일부 속성이 변경됩니다.

function example(a, b, c) {
  return a + b + c;
}

console.log(example.length); // 3 (매개변수 개수)
console.log(example.name);   // "example"

// 인자 없이 bind
const bound1 = example.bind(null);
console.log(bound1.length);  // 3
console.log(bound1.name);    // "bound example"

// 인자 1개 바인딩
const bound2 = example.bind(null, 1);
console.log(bound2.length);  // 2 (남은 매개변수 개수)
console.log(bound2.name);    // "bound example"

// 인자 2개 바인딩
const bound3 = example.bind(null, 1, 2);
console.log(bound3.length);  // 1
console.log(bound3.name);    // "bound example"

주의: 함수의 length 속성에 의존하는 라이브러리를 사용할 때 문제가 될 수 있습니다.

5. 성능 고려사항

bind()는 새 함수를 생성하므로 성능에 영향을 줄 수 있습니다.

class Component {
  constructor() {
    this.count = 0;
  }

  handleClick() {
    this.count++;
  }

  // ❌ 매우 나쁜 예: 렌더링마다 새로운 함수 생성
  renderBad() {
    return `<button onclick="${this.handleClick.bind(this)}">클릭</button>`;
    // 렌더링할 때마다 새 함수 생성 → 메모리 낭비 + 성능 저하
  }

  // ✅ 좋은 예 1: 생성자에서 한 번만 바인딩
  constructor() {
    this.count = 0;
    this.handleClick = this.handleClick.bind(this);
  }

  renderGood1() {
    return `<button onclick="${this.handleClick}">클릭</button>`;
  }

  // ✅ 좋은 예 2: 화살표 함수 사용
  handleClickArrow = () => {
    this.count++;
  }

  renderGood2() {
    return `<button onclick="${this.handleClickArrow}">클릭</button>`;
  }
}

React에서의 성능 비교:

class TodoItem extends React.Component {
  // ❌ 최악: render에서 bind
  render() {
    return (
      <button onClick={this.handleClick.bind(this)}>
        {/* 렌더링마다 새 함수 → shouldComponentUpdate 무용지물 */}
      </button>
    );
  }

  // ⚠️ 나쁨: render에서 화살표 함수
  render() {
    return (
      <button onClick={() => this.handleClick()}>
        {/* 렌더링마다 새 함수 → shouldComponentUpdate 무용지물 */}
      </button>
    );
  }

  // ✅ 좋음: 생성자에서 bind
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {/* 같은 함수 참조 → 성능 최적화 가능 */}
      </button>
    );
  }

  // ✅ 가장 좋음: 클래스 필드
  handleClick = () => {
    // 인스턴스당 한 번만 생성
  }

  render() {
    return <button onClick={this.handleClick} />;
  }
}

6. 이벤트 리스너 제거 문제

bind()로 생성한 함수는 removeEventListener로 제거할 수 없습니다.

class Button {
  constructor() {
    this.count = 0;
    this.button = document.querySelector('#myButton');
  }

  handleClick() {
    this.count++;
    console.log(this.count);
  }

  // ❌ 나쁜 예: 이벤트 제거 불가능
  attachBad() {
    this.button.addEventListener('click', this.handleClick.bind(this));

    // 나중에 제거 시도
    this.button.removeEventListener('click', this.handleClick.bind(this));
    // 작동 안 함! bind(this)는 매번 새 함수를 생성함
  }

  // ✅ 좋은 예: 참조 저장
  attachGood() {
    this.boundHandler = this.handleClick.bind(this);
    this.button.addEventListener('click', this.boundHandler);
  }

  detach() {
    this.button.removeEventListener('click', this.boundHandler);
    // 정상 작동!
  }

  // ✅ 더 좋은 예: 생성자에서 bind
  constructor() {
    this.count = 0;
    this.button = document.querySelector('#myButton');
    this.handleClick = this.handleClick.bind(this); // 한 번만 bind
  }

  attach() {
    this.button.addEventListener('click', this.handleClick);
  }

  detach() {
    this.button.removeEventListener('click', this.handleClick);
  }
}

bind vs call vs apply

JavaScript에는 this를 제어하는 세 가지 메소드가 있습니다.

비교표

메소드 실행 시점 this 바인딩 인자 전달 방식 반환값
bind() 나중에 실행 영구적 개별 인자 새 함수
call() 즉시 실행 일시적 개별 인자 함수 실행 결과
apply() 즉시 실행 일시적 배열 함수 실행 결과

예제 비교

const person = {
  name: 'Alice',
  greet(greeting, punctuation) {
    return `${greeting}, ${this.name}${punctuation}`;
  }
};

const anotherPerson = { name: 'Bob' };

// bind: 새 함수 생성 (나중에 호출)
const greetBob = person.greet.bind(anotherPerson, 'Hello');
console.log(greetBob('!'));  // "Hello, Bob!"
console.log(greetBob('?'));  // "Hello, Bob?" (첫 번째 인자는 고정됨)

// call: 즉시 실행 (개별 인자)
const result1 = person.greet.call(anotherPerson, 'Hello', '!');
console.log(result1);  // "Hello, Bob!"

// apply: 즉시 실행 (배열 인자)
const result2 = person.greet.apply(anotherPerson, ['Hello', '!']);
console.log(result2);  // "Hello, Bob!"

언제 무엇을 사용할까?

bind 사용:

// ✅ 콜백으로 전달할 함수가 필요할 때
button.addEventListener('click', this.handleClick.bind(this));
setTimeout(this.update.bind(this), 1000);

// ✅ 부분 적용이 필요할 때
const add5 = add.bind(null, 5);

// ✅ 메소드를 독립 함수로 변환할 때
const boundMethod = obj.method.bind(obj);

call 사용:

// ✅ 즉시 실행하되 this를 명시하고 싶을 때
function greet() {
  console.log(`Hello, ${this.name}`);
}
greet.call({ name: 'Alice' }); // "Hello, Alice"

// ✅ 배열 메소드를 유사 배열에 사용할 때
Array.prototype.push.call(arrayLike, newItem);

// ✅ 상속 구현
function Parent(name) {
  this.name = name;
}

function Child(name, age) {
  Parent.call(this, name); // 부모 생성자 호출
  this.age = age;
}

apply 사용:

// ✅ 인자가 배열일 때
const numbers = [1, 2, 3, 4, 5];
const max = Math.max.apply(null, numbers);
console.log(max); // 5

// ✅ 가변 인자 함수에 배열 전달
function sum(a, b, c) {
  return a + b + c;
}
const args = [1, 2, 3];
console.log(sum.apply(null, args)); // 6

// 현대적 방법: spread 연산자
console.log(Math.max(...numbers)); // 5
console.log(sum(...args)); // 6

실전 비교 예제

const obj = {
  value: 42,
  getValue() {
    return this.value;
  }
};

const obj2 = { value: 100 };

// bind: 재사용 가능한 함수 생성
const boundGetValue = obj.getValue.bind(obj2);
console.log(boundGetValue()); // 100
console.log(boundGetValue()); // 100
// 여러 번 호출 가능

// call: 즉시 실행
console.log(obj.getValue.call(obj2)); // 100
// 매번 호출 필요

// apply: 즉시 실행 (배열 인자)
console.log(obj.getValue.apply(obj2)); // 100
// 매번 호출 필요

대안: 화살표 함수

많은 경우 화살표 함수가 bind()보다 더 간결하고 읽기 쉬운 대안입니다.

bind 사용

class Component {
  constructor() {
    this.state = { count: 0 };

    // 생성자에서 bind
    this.increment = this.increment.bind(this);
    this.decrement = this.decrement.bind(this);
    this.reset = this.reset.bind(this);
  }

  increment() {
    this.state.count++;
  }

  decrement() {
    this.state.count--;
  }

  reset() {
    this.state.count = 0;
  }
}

화살표 함수 사용 (더 현대적)

class Component {
  state = { count: 0 };

  // 클래스 필드로 화살표 함수 정의
  increment = () => {
    this.state.count++;
  }

  decrement = () => {
    this.state.count--;
  }

  reset = () => {
    this.state.count = 0;
  }
}

장단점 비교

bind():

장점:

  • ✅ 프로토타입 메소드로 정의됨 (메모리 효율적)
  • ✅ 모든 브라우저/환경에서 지원
  • ✅ 명시적이고 전통적인 방식

단점:

  • ❌ 보일러플레이트 코드 증가
  • ❌ 생성자가 복잡해짐
  • ❌ 실수하기 쉬움 (bind 빼먹기)

화살표 함수:

장점:

  • ✅ 간결하고 읽기 쉬움
  • ✅ bind를 깜빡할 일이 없음
  • ✅ 현대적이고 직관적

단점:

  • ❌ 인스턴스마다 함수 생성 (메모리 사용량 증가)
  • ❌ 프로토타입 메소드로 정의 불가
  • ❌ 구형 환경에서는 트랜스파일 필요

언제 bind를 사용할까?

bind가 더 적합한 경우:

// 1. 부분 적용이 필요할 때
const add = (a, b, c) => a + b + c;
const add5 = add.bind(null, 5);
console.log(add5(10, 20)); // 35

// 화살표 함수로는 부분 적용 불가능

// 2. 기존 메소드의 this를 변경해야 할 때
const obj1 = {
  name: 'Object 1',
  greet() { return `Hello, ${this.name}`; }
};

const obj2 = { name: 'Object 2' };

const greetAsObj2 = obj1.greet.bind(obj2);
console.log(greetAsObj2()); // "Hello, Object 2"

// 3. 프로토타입 메소드를 정의할 때
class Animal {
  constructor(name) {
    this.name = name;
  }
}

// 프로토타입에 메소드 추가
Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound`);
};

// bind로 this 고정
const dog = new Animal('Dog');
const speak = dog.speak.bind(dog);
speak(); // "Dog makes a sound"

화살표 함수가 더 적합한 경우:

// 1. 이벤트 핸들러
class Button {
  count = 0;

  // ✅ 화살표 함수
  handleClick = () => {
    this.count++;
  }

  // ❌ bind 필요
  handleClickOld() {
    this.count++;
  }

  constructor() {
    this.handleClickOld = this.handleClickOld.bind(this);
  }
}

// 2. 콜백 함수
class DataFetcher {
  data = [];

  // ✅ 화살표 함수
  fetchData = () => {
    fetch('/api/data')
      .then(response => response.json())
      .then(data => {
        this.data = data; // this가 자동으로 유지됨
      });
  }

  // ❌ bind 필요
  fetchDataOld() {
    fetch('/api/data')
      .then(response => response.json())
      .then(function(data) {
        this.data = data; // this 손실
      }.bind(this));
  }
}

실전에서 활용하기

1. API 클라이언트 패턴

class APIClient {
  constructor(baseURL, apiKey) {
    this.baseURL = baseURL;
    this.apiKey = apiKey;
    this.headers = {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}`
    };

    // 모든 메소드를 바인딩하여 독립적으로 사용 가능하게 함
    this.get = this.get.bind(this);
    this.post = this.post.bind(this);
    this.put = this.put.bind(this);
    this.delete = this.delete.bind(this);
  }

  async request(endpoint, options) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      ...options,
      headers: { ...this.headers, ...options.headers }
    };

    const response = await fetch(url, config);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return response.json();
  }

  get(endpoint) {
    return this.request(endpoint, { method: 'GET' });
  }

  post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }

  put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }

  delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }
}

// 사용 예
const api = new APIClient('https://api.example.com', 'your-api-key');

// 메소드를 독립적으로 전달 가능
const fetchUser = api.get;
const createUser = api.post;

// 어디서든 호출 가능
fetchUser('/users/1').then(console.log);

createUser('/users', { name: 'Alice', email: 'alice@example.com' })
  .then(console.log);

// 배열 메소드와 함께 사용
const userIds = [1, 2, 3, 4, 5];
Promise.all(userIds.map(id => fetchUser(`/users/${id}`)))
  .then(users => console.log('모든 사용자:', users));

2. 함수 파이프라인 패턴

// 부분 적용을 활용한 함수 조합
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
const subtract = (a, b) => a - b;
const divide = (a, b) => a / b;

// 특정 값으로 연산하는 함수들 생성
const add10 = add.bind(null, 10);
const multiplyBy2 = multiply.bind(null, 2);
const subtract5 = subtract.bind(null, 5);
const divideBy3 = divide.bind(null, 3);

// 파이프라인 함수
function pipe(...fns) {
  return (value) => fns.reduce((acc, fn) => fn(acc), value);
}

// 파이프라인 구성
const calculate = pipe(
  add10,        // 5 + 10 = 15
  multiplyBy2,  // 15 * 2 = 30
  subtract5,    // 30 - 5 = 25
  divideBy3     // 25 / 3 = 8.333...
);

console.log(calculate(5)); // 8.333...

// 실전 활용: 데이터 변환 파이프라인
const trim = (str) => str.trim();
const toLowerCase = (str) => str.toLowerCase();
const removeSpaces = (str) => str.replace(/\s+/g, '-');
const truncate = (len, str) => str.slice(0, len);

const slugify = pipe(
  trim,
  toLowerCase,
  removeSpaces,
  truncate.bind(null, 50)
);

console.log(slugify('  Hello World From JavaScript  '));
// "hello-world-from-javascript"

3. 유효성 검사기 패턴

// 범용 검사 함수
function validate(rules, value) {
  return rules.every(rule => rule(value));
}

// 검사 규칙 정의
function isInRange(min, max, value) {
  return value >= min && value <= max;
}

function hasLength(min, max, value) {
  const length = typeof value === 'string' ? value.length : 0;
  return length >= min && length <= max;
}

function matches(regex, value) {
  return regex.test(value);
}

function isEmail(value) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(value);
}

// 특정 검사기 생성 (부분 적용)
const isAdultAge = isInRange.bind(null, 18, 120);
const isChildAge = isInRange.bind(null, 0, 17);
const isValidUsername = hasLength.bind(null, 3, 20);
const isValidPassword = hasLength.bind(null, 8, 50);
const isNumeric = matches.bind(null, /^\d+$/);
const isAlphabetic = matches.bind(null, /^[a-zA-Z]+$/);

// 복합 검사기
const validateUser = (user) => {
  const errors = [];

  if (!isValidUsername(user.username)) {
    errors.push('사용자명은 3-20자여야 합니다');
  }

  if (!isValidPassword(user.password)) {
    errors.push('비밀번호는 8-50자여야 합니다');
  }

  if (!isEmail(user.email)) {
    errors.push('유효한 이메일 주소가 아닙니다');
  }

  if (!isAdultAge(user.age)) {
    errors.push('18세 이상만 가입 가능합니다');
  }

  return {
    valid: errors.length === 0,
    errors
  };
};

// 사용 예
const user = {
  username: 'alice',
  password: 'password123',
  email: 'alice@example.com',
  age: 25
};

const result = validateUser(user);
console.log(result); // { valid: true, errors: [] }

4. 디바운스/쓰로틀과 함께 사용

class SearchBox {
  constructor(inputElement) {
    this.input = inputElement;
    this.results = [];

    // 디바운스된 검색 함수 생성
    this.debouncedSearch = this.debounce(
      this.performSearch.bind(this),
      300
    );

    // 이벤트 리스너 등록
    this.input.addEventListener('input', this.handleInput.bind(this));
  }

  performSearch(query) {
    console.log(`검색 중: "${query}"`);

    // API 호출
    fetch(`/api/search?q=${encodeURIComponent(query)}`)
      .then(res => res.json())
      .then(data => {
        this.results = data;
        this.displayResults();
      })
      .catch(err => {
        console.error('검색 실패:', err);
      });
  }

  handleInput(event) {
    const query = event.target.value.trim();

    if (query.length >= 2) {
      this.debouncedSearch(query);
    } else {
      this.results = [];
      this.displayResults();
    }
  }

  displayResults() {
    console.log('검색 결과:', this.results);
    // UI 업데이트 로직
  }

  debounce(func, wait) {
    let timeout;
    return function(...args) {
      clearTimeout(timeout);
      timeout = setTimeout(() => func.apply(this, args), wait);
    };
  }
}

// 사용 예
const searchBox = new SearchBox(document.querySelector('#search-input'));

5. 로거 패턴

// 범용 로그 함수
function log(level, category, timestamp, message, ...data) {
  const logMessage = `[${level}] [${category}] ${timestamp}: ${message}`;

  switch (level) {
    case 'ERROR':
      console.error(logMessage, ...data);
      break;
    case 'WARN':
      console.warn(logMessage, ...data);
      break;
    case 'INFO':
      console.info(logMessage, ...data);
      break;
    default:
      console.log(logMessage, ...data);
  }
}

// 레벨별 로거 생성
const errorLog = log.bind(null, 'ERROR');
const warnLog = log.bind(null, 'WARN');
const infoLog = log.bind(null, 'INFO');
const debugLog = log.bind(null, 'DEBUG');

// 카테고리별 로거 생성
const dbErrorLog = errorLog.bind(null, 'DATABASE');
const authWarnLog = warnLog.bind(null, 'AUTH');
const apiInfoLog = infoLog.bind(null, 'API');

// 타임스탬프 포함 로거
function getCurrentTimestamp() {
  return new Date().toISOString();
}

// 사용 예
dbErrorLog(getCurrentTimestamp(), '데이터베이스 연결 실패', { host: 'localhost', port: 5432 });
// [ERROR] [DATABASE] 2025-11-20T10:00:00.000Z: 데이터베이스 연결 실패 { host: 'localhost', port: 5432 }

authWarnLog(getCurrentTimestamp(), '로그인 시도 3회 실패', { username: 'user123' });
// [WARN] [AUTH] 2025-11-20T10:01:00.000Z: 로그인 시도 3회 실패 { username: 'user123' }

apiInfoLog(getCurrentTimestamp(), 'API 요청 성공', { endpoint: '/users', method: 'GET' });
// [INFO] [API] 2025-11-20T10:02:00.000Z: API 요청 성공 { endpoint: '/users', method: 'GET' }

결론: bind를 언제 어떻게 사용할까?

bind를 이해하면

  1. this가 사라지는 문제를 해결할 수 있습니다
  2. 재사용 가능한 함수를 쉽게 만들 수 있습니다
  3. 함수형 프로그래밍 패턴을 활용할 수 있습니다
  4. 이벤트 핸들러와 콜백을 올바르게 다룰 수 있습니다

핵심 원칙

1. bind()는 새로운 함수를 생성한다
2. this는 영구적으로 고정된다
3. 인자를 미리 설정할 수 있다 (부분 적용)
4. 한 번 바인딩하면 다시 변경 불가능
5. 화살표 함수는 bind할 수 없다

언제 bind를 사용할까?

bind 사용:

  • 콜백이나 이벤트 핸들러로 메소드 전달
  • 부분 적용이 필요할 때
  • 메소드를 독립 함수로 변환
  • 프로토타입 메소드 정의

화살표 함수 사용:

  • 클래스 메소드 정의 (현대적)
  • 간단한 콜백 함수
  • 렉시컬 this가 필요한 경우

call/apply 사용:

  • 즉시 실행이 필요할 때
  • 배열 메소드 차용
  • 생성자 체이닝

기억해야 할 것

  1. bind()는 this를 영구적으로 고정합니다
  2. 부분 적용으로 재사용 가능한 함수를 만들 수 있습니다
  3. 성능을 위해 생성자에서 한 번만 bind하세요
  4. 화살표 함수가 더 나은 대안일 수 있습니다
  5. 이벤트 제거를 위해 참조를 저장하세요

bind()를 이해하면 JavaScript의 this를 완전히 제어할 수 있습니다!

참고 자료

공식 문서

심화 학습

관련 문서

댓글