티스토리 뷰

안녕하세요. 코어 자바스크립트를 읽다가 3장 this에서 call, apply, bind 함수 사용법을 알게되어 블로그에 기록합니다. 책을 읽으면서 가장 궁금했던 'call, apply, bind를 언제 사용할까?'을 중심으로 설명하고, call, apply, bind 대신 사용할 수 있는 코드도 함께 살펴보고자 합니다.

호랑이 책 '코어 자바스크립트'

call, apply, bind 사용 사례

서적과 인터넷 자료를 보면서 찾아보고, 세 함수를 사용하는 대표적인 상황은 아래처럼 정리했습니다.

 

1. 다른 객체의 메소드 사용하기 (내부 함수에 this 지정하기) - call, apply, bind

2. 생성자에서 다른 생성자 호출하기 (생성자 함수에 this 지정하기) - call, apply

3. 콜백 함수를 메소드처럼 호출하기 (콜백 함수에 this 지정하기) - bind

다른 객체의 메소드 사용하기 (내부 함수에 this 지정하기)

call, apply, bind 함수를 사용하여 다른 객체의 메소드를 사용할 수 있는데요. 예시로, 유사배열객체에 배열 함수를 사용하는 방법과 숫자 배열에 Math의 max, min 함수를 사용하는 방법을 살펴보겠습니다.

1️⃣ 유사배열객체에 배열 메소드 사용하기

유사배열객체(Array-like Object)는 0이나 양의 정수 그리고 length를 프로퍼티로 가진, 배열과 유사한 구조의 객체입니다. 대표적으로 함수 내에서 사용할 수 있는 arguments 객체와 querySelectorAll 메소드 등으로 가져온 NodeList가 있습니다. 문자열은 length가 읽기 전용이지만, 인덱스와 length 프로퍼티를 가진 유사배열객체입니다.

 

유사배열객체는 배열과 구조가 유사하지만 배열의 메소드를 모두 사용할 수 없는데요. call 함수를 사용하여 slice 배열 함수에 유사배열객체를 this로 지정하면, 해당 객체를 배열로 전환하여 배열 함수를 사용할 수 있습니다.

 

call 함수의 구조는 다음과 같습니다. thisArg에 this로 지정할 객체를 넘겨줍니다.

// 출처: MDN
func.call(thisArg[, arg1[, arg2[, ...]]])

NodeList에 배열 함수 사용하기

html 코드에 있는 리스트를 querySelectorAll로 불러오면 NodeList가 반환되는데, pop 함수를 사용하려고 하면 에러가 발생합니다.

 

querySelectorAll가 반환한 NodeList

const nodeList = document.querySelectorAll("li");
nodeList.pop(); // index.js:2 Uncaught TypeError: lists.pop is not a function

대신 NodeList를 slice 함수에 this로 지정하여 배열로 변환하면 pop 함수를 사용할 수 있습니다.

const nodeList = document.querySelectorAll("li");
const arr = Array.prototype.slice.call(nodeList);

arr.pop();
console.log(arr);

콘솔에 출력된 arr의 모습은 다음과 같습니다. 5개였던 리스트가 pop 함수로 인해 4개로 줄어들었습니다.

 

배열로 변환된 모습

문자열에 배열 함수 사용하기

이번에는 map 함수에 call을 사용하여 문자열 'banana'의 'a'를 빈칸 '_'로 만들어보겠습니다. 아래 코드에서 map 함수의 this를 str로 지정하자 map 함수가 문자열을 순회하며 콜백 함수를 실행하는 걸 확인할 수 있습니다.

const str = "banana";
const arr = Array.prototype.map.call(str, function (char) {
  if (char === "a") return "_";
  return char;
});

console.log(arr.join("")); // b_n_n_

하지만 아래처럼 문자열에 map 함수를 바로 사용하려고 하면 존재하지 않는 함수라며 에러가 발생합니다.

const str = "banana";
const arr = str.map(function (char) {
  if (char === "a") return "_";
  return char;
}); // Uncaught TypeError: str.map is not a function

arguments에 배열 함수 사용하기

마지막으로 arguments와 배열 함수인 reduce를 활용하여 sum 함수를 구현해보겠습니다. sum 함수의 arguments에 reduce를 사용하면 이전 사례와 마찬가지로 에러가 발생합니다.

function sum() {
  const result = arguments.reduce(function (a, b) {
    return a + b;
  }); // Uncaught TypeError: arguments.reduce is not a function at sum

  return result;
}

console.log(sum(1, 2, 3));

하지만 아래처럼 call 함수로 reduce의 this를 arguments로 지정해주면, reduce 함수를 사용할 수 있습니다. sum 함수에 전달한 인수가 arguments에 저장되고, reduce 함수에서 합산되어 그 결과를 반환합니다.

function sum() {
  const result = Array.prototype.reduce.call(arguments, function (a, b) {
    return a + b;
  });

  return result;
}

console.log(sum(1, 2, 3)); // 6

ES6의 Array.from()

ES6에서는 Array.from() 함수를 사용하여 유사배열객체를 얕게 복사하여 배열로 반환할 수 있습니다. 즉, 위에서 사용한 NodeList와 문자열, arguments를 다음과 같이 배열로 받을 수 있습니다.

// NodeList를 배열로 복사하기
const nodeList = document.querySelectorAll("li");
const nodeArr = Array.from(nodeList);
console.log(nodeArr);

// 문자열을 배열로 복사하기
const str = "banana";
const strArr = Array.from(str);
console.log(strArr);

// arguments를 배열로 복사하기
function sum() {
  const arr = Array.from(arguments);
  const result = arr.reduce(function (a, b) {
    return a + b;
  });
  return result;
}

console.log(sum(1, 2, 3)); // 6

각 배열을 출력한 결과는 다음과 같습니다. NodeList, 문자열, 숫자 인수 모두 배열로 변환되었습니다.

왼쪽부터 NodeList, 문자열, arguments에서 복사한 배열

2️⃣ Math의 max, min 함수에 배열 넘기기

이번에는 apply 함수를 사용하여 내부 함수의 매개변수로 배열을 전달해보겠습니다. 코어 JS(p.86)에 소개된 대표적인 예시로는 Math.max() 함수와 Math.min() 함수가 있습니다. 두 함수는 모두 매개변수로 여러 인자를 받을 수 있습니다.

// 출처: MDN
Math.max(value1, value2, /* …, */ valueN)
Math.min(value1, value2, /* …, */ valueN)

하지만 여러 변수를 넘기는 대신 배열에서 제일 큰 수를 찾고 싶을 땐? 다음 코드처럼 apply 함수로 배열을 넘기면 됩니다. 배열의 숫자를 일일이 인수를 입력하지 않고 한 번에 전달할 수 있습니다.

const arr = [-3, 2, 3, 4, 5, 10];
const maxNum = Math.max.apply(null, arr);
const minNum = Math.min.apply(null, arr);

console.log(maxNum, minNum); // 10 -3

ES6의 전개/펼치기 연산자(Spread operator)

다른 방법으로는 ES6에서 나온 전개 연산자 또는 펼치기 연사자를 사용하여, 하나의 배열을 여러 인수로 전달할 수 있습니다. 전개 연산자를 사용하면, 배열의 인수를 모두 입력한 것과 같은 결과가 나옵니다.

const arr = [-3, 2, 3, 4, 5, 10];
const maxNum = Math.max(...arr);
const minNum = Math.min(...arr);

console.log(maxNum, minNum); // 10 -3

생성자에서 다른 생성자 호출하기 (생성자 함수에 this 지정하기)

이번에는 다른 생성자와 공통되는 속성을 반복해서 적지 않고, call 함수를 사용하여 다른 생성자를 호출하는 사례입니다. 코어 JS와 MDN에 모두 나와있는 방법입니다. 아래는 다른 생성자에서도 반복되는 name과 age 속성을 Animal 생성자를 호출하여 작성한 코드입니다.

function Animal(name, age) {
  this.name = name;
  this.age = age;
}

function Cat(name, age) {
  Animal.call(this, name, age);
  this.cry = "야옹";
}

function Dog(name, age) {
  Animal.call(this, name, age);
  this.bark = "멍멍";
}

const leo = new Cat("레오", 3); // Cat {name: '레오', age: 3, cry: '야옹'}
const bori = new Dog("보리", 1); // Dog {name: '보리', age: 1, bark: '멍멍'}

공통된 name과 age 속성은 Animal 생성자 함수를 call로 호출하여 넘기고, 그 외의 속성만 추가로 작성하면 됩니다.

콜백 함수를 메소드처럼 호출하기 (콜백 함수에 this 지정하기)

콜백 함수를 호출할 때는 this가 따로 지정하지 않으면 this가 전역 객체를 가리키게 됩니다. 하지만 함수를 반환하는 bind 함수를 사용하면 콜백 함수에 this를 지정할 수 있습니다. 사용 사례로 addEventListener와 React의 클래스형 컴포넌트의 이벤트 핸들러를 살펴보고자 합니다.

1️⃣ addEventListener와 콜백 함수

예시로 버튼을 누르면 식물에게 물을 주는 기능을 만들어보겠습니다. 식물 정보를 담은 객체를 plant 변수에 할당하고, 객체 내부에 정의한 함수를 button의 이벤트 핸들러로 전달하겠습니다.

const btns = document.querySelectorAll("button");

const plant = {
  water: 0,
  sun: 10,

  giveWater() {
    ++this.water;
    console.log(this); // button
  },
};

btns.forEach(function (btn) {
  btn.addEventListener("click", plant.giveWater);
});

위 코드를 보면 버튼을 누를 때마다 water 값이 증가할 거 같지만, addEventListener는 HTML element를 this로 전달하기 때문에 식물의 water 값이 증가하지 않습니다. giveWater 앞에 plant가 명시되어 있어도 함수만 전달될 뿐, 콜백 함수는 메소드로 실행되지 않습니다. 실제로 giveWater() 함수에서 this를 출력해보면 button이 나오는 것을 확인할 수 있습니다.

 

이벤트 핸들러의 this (출처: MDN)

 

이를 해결하려면 giveWater에 bind 함수를 사용하여 plant를 this로 지정하면 됩니다.

const btns = document.querySelectorAll("button");

const plant = {
  water: 0,
  sun: 10,

  giveWater() {
    ++this.water;
    console.log(this);
  },
};

btns.forEach(function (btn) {
  btn.addEventListener("click", plant.giveWater.bind(plant));
});

콘솔에 출력된 this를 확인해보면, plant 객체가 나오고 water 값도 잘 증가하는 것을 확인할 수 있습니다.

 

식물에게 물을 줄 수 있게 됐습니다💧

plant, plant 반복되는데 다른 방법 없나?

plant가 앞뒤로 두 번 반복되니 외관상 별로라는 생각이 듭니다. plant.giveWater()라고 쓴 김에 메소드로 실행되면 좋을텐데요. 그러면 그냥 한 번더 함수로 감싸주면 됩니다.

// ...

btns.forEach(function (btn) {
  btn.addEventListener("click", function () {
    plant.giveWater();
  });
});

이렇게 하면  전달한 이벤트 함수에서는 this가 button으로 지정되지만, giveWater()는 원래대로 메소드로 실행되기 때문에 this로 plant가 지정됩니다.

2️⃣ React의 클래스형 컴포넌트와 콜백 함수

bind의 쓰임새에 대해서 찾다가 클래스형 컴포넌트에 함수를 넘기는 방법을 설명한 이전 React 문서를 찾았습니다. ES 버전 별로 다양한 방법을 설명해서, 여기에 정리해보려고 합니다.

 

비슷한 예제로 버튼을 누르면 state의 water 값이 1 증가하는 코드를 아래에 작성하였습니다.

import { Component } from "react";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      water: 0,
      sun: 10,
    };
  }

  giveWater() {
    console.log(this); // undefined
    this.setState(function (prev) {
      return { water: ++prev.water };
    });
  }

  render() {
    return <button onClick={this.giveWater}>Click Me</button>;
  }
}

export default App;

이전의 설명과 마찬가지로 this.giveWater만 넘기면 콜백 함수는 this를 지정하지 않고 함수로 실행됩니다. 버튼을 눌러보면 this는 undefined(모듈에서는 자동으로 엄격 모드가 적용되어 전역 객체가 아닌 undefined가 됩니다)로 출력되며, setState 프로퍼티를 찾을 수 없다는 에러가 발생합니다.

 

이를 해결하려면 this.giveWater를 넘길 때 bind 함수를 사용하여 this를 지정해주면 됩니다. 코드는 아래와 같습니다.

// ...
constructor(props) {
super(props);
this.state = {
  water: 0,
  sun: 10,
};
}

giveWater() {
console.log(this); // App {...}
this.setState(function (prev) {
  return { water: ++prev.water };
});
}

render() {
return <button onClick={this.giveWater.bind(this)}>Click Me</button>;
}

// ...

this를 출력해보면 this가 App 객체로 지정되어 있는 걸 확인할 수 있습니다. App의 state도 살펴보면, water 값도 1씩 잘 증가하네요!

 

컴포넌트를 this로 지정

하지만 onClick에 넘어가는 함수가 너무 길어져서 줄이고 싶으면, 생성자 함수에서 함수를 바인딩한 후 넘겨주는 방법도 있습니다.

import { Component } from "react";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      water: 0,
      sun: 10,
    };
    this.giveWater = this.giveWater.bind(this); // 여기서 바인딩!
  }

  giveWater() {
    console.log(this); // App {...}
    this.setState(function (prev) {
      return { water: ++prev.water };
    });
  }

  render() {
    return <button onClick={this.giveWater}>Click Me</button>;
  }
}

export default App;

화살표 함수(Arrow function)

앞에서 열심히 다양한 방법을 살펴봤지만, 외관상 가장 간단한 방법이 있습니다. 바로 ES6에 나온 화살표 함수를 사용하는 것입니다. 화살표 함수는 기존 함수와 다르게 함수가 생성될 때 this를 바인딩하는 과정 자체가 없습니다. 즉, 화살표 함수에서 this를 사용하면, 상위 컨텍스트로 거슬러 올라가며 this를 찾습니다.

 

화살표 함수 설명 일부 (출처: MDN)

 

예시 코드에서 giveWater 함수를 화살표 함수로 바꿔주면, giveWater에서 사용된 this는 render 함수에서 찾은 this를 적용하게 됩니다. 아래 코드를 보면, 생성자 함수나 bind 함수를 사용하지 않고 깔끔한 모습입니다.

// ...

giveWater = () => {
    console.log(this); // App {...}
    this.setState(function (prev) {
      return { water: ++prev.water };
    });
};

render() {
    console.log(this); // App {...} 
    return <button onClick={this.giveWater}>Click Me</button>;
}

//...

call, apply, bind 함수의 쓰임새와 함께, 이 함수를 대체할 수 있는 Array.from(), 전개/펼치기 연산자, 화살표 함수도 살펴봤습니다. 저도 글을 작성하며 공부가 많이 되었네요. 다음에는 비동기 함수를 다루는 Promise와 Generator에 대해 공부하고자 합니다. 읽어주셔서 감사합니다!

 

 

(아직 많이 배우는 중입니다. 잘못된 부분 찝어주는 댓글 환영입니다🙇)

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함