왓 더 훅? Tapable에 대하여 알아보자

cadenzah·2021년 8월 23일
2

Webpack

목록 보기
1/1
post-thumbnail
  • 이 글은 What the Hook? Learn the basics of Tapable를 번역한 글입니다.
  • 매끄러운 문맥을 위하여 의역을 한 경우가 있습니다. 원문의 뜻을 최대한 해치지 않도록 노력했으니 안심하셔도 됩니다.
  • 영어 단어가 자연스러운 경우 원문 그대로의 영단어를 적었습니다.
  • 저의 보충 설명은 인용문 또는 괄호 안에 역자 주 문구를 통하여 달았습니다.

Tapable이란

Tapable은 유명한 모듈 번들러 Webpack를 구성하는 핵심 라이브러리로, 플러그인 용(Hook)을 만드는 데에 사용됩니다. Webpack이 강력한 이유 중 하나는 직접 플러그인을 작성할 수 있다는 점 때문인데요, 훅을 사용하는 강력한 커스텀 플러그인 시스템은 Tapable을 통하여 구현됩니다.

Hook이란

훅을 사용하면, 중요한 이벤트가 발생되었을 때에 알림을 받거나 코드를 실행하도록 만들 수 있습니다. 아마 비슷한 것을 보신 적이 있을 겁니다. 브라우저를 예로 들면, 브라우저는 여러분이 탭할 수 있는(tap into) 다양한 훅을 제공합니다. 클릭 이벤트가 발생할 때 어떤 코드가 실행되도록 만들고 싶다면, 아래와 같은 코드를 작성하면 됩니다.

역자 주: tap [tæp] 1. (가볍게) 톡톡 두드리다 , ... 4. (전화 도청 장치를 이용해서) 도청하다

Tap이란 임의의 Hook을 대상으로 이루어지는 것으로, 어떤 훅을 탭한다는 것은 훅이 발생할 때에 실행시킬 코드를 등록한다는 것으로 이해할 수 있을 것 같습니다. 그러면 앞으로 해당 훅을 예의 주시하고 있다가, 훅시 발생할 때마다 해당 코드가 실행될 것입니다. 아래에 저자가 설명하겠지만 JavaScript의 이벤트 리스너와 유사한 메커니즘입니다.

// 사용자가 화면을 클릭했을 때 콘솔 창에 메시지가 출력된다
document.addEventListener("click", function() {
    console.log("You clicked me!");
})

훅의 또다른 예로는 React 생명 주기 메서드가 있습니다. React를 사용해보셨다면, componentDidMount, componentDidUpdate, componentWillUnmount 등을 들어본 적이 있을 것입니다. 생명 주기 메서드 또한 훅과 비슷한 것입니다. 아래와 같이, 특정 생명 주기 이벤트가 발생할 때마다 실행될 코드를 추가할 수 있습니다.

import React from "react";

// 이 컴포넌트가 화면 상에 마운트되었을 때, 콘솔 창에 메시지가 출력된다
class MyComponent extends React.Component {
    componentDidMount() {
        console.log("I mounted onto the screen!");
    }

    render() {
        return <h1>🎣</h1>
    }
}

새로 나온 React Hooks API를 사용하면 컴포넌트 클래스 바깥에서도 생명 주기 메서드를 사용할 수 있습니다.

Tapable을 사용하여 Hook 만들기

Tapable이 제공하는 가장 기본적인 훅은 동기 훅, 즉 SyncHook입니다. 아래와 같이 생성할 수 있습니다.

import { SyncHook } from "tapable";

const newHook = new SyncHook();

Tapable에서는 hooks 속성을 통하여 훅을 노출할 것을 권장합니다. 그러면 탭할 수 있는 훅이 어떤 것이 있는지 개발자가 쉽게 파악할 수 있습니다.

import { SyncHook } from "tapable";

class Car {
  constructor() {
    this.hooks = {
      startCar: new SyncHook()
    };
  }
}

Webpack에서는 플러그인 개발자에게 아래와 같은 방식으로 훅을 노출하고 있습니다.

// https://webpack.js.org/contribute/writing-a-plugin/ 에서 가져온 예제
class MyExampleWebpackPlugin {
  apply(compiler) {
    // `compiler.hooks`에 주목하시기 바랍니다 🎣
    compiler.hooks.emit.tapAsync(
      'MyExampleWebpackPlugin',
      (compilation, callback) => {
        console.log('This is an example plugin!');

        callback();
      }
    );
  }
}

훅을 발생시키려면 call 메서드를 실행하면 됩니다.

import { SyncHook } from "tapable";

class Car {
  constructor() {
    this.hooks = {
      carStarted: new SyncHook()
    };
  }

  turnOn() {
    this.hooks.carStarted.call();
  }
}

const myCar = new Car();
myCar.turnOn();

이 훅이 발생되면, 해당 훅을 탭하고 있는 모든 함수들이 실행됩니다.

앞서 다룬 클릭 이벤트 리스너를 떠올려보시면 좋습니다. 훅을 호출하는 것은 "click" 이벤트를 트리거하는 것과 비슷합니다. 모든 클릭 이벤트 리스터 함수가 다같이 실행될 것입니다.

call 메서드에 인자를 전달할 수도 있습니다. 이 경우, 훅을 초기화할 때 인자 이름을 반드시 추가해야 합니다.

import { SyncHook } from "tapable";

class Car {
  constructor() {
    this.hooks = {
      carStarted: new SyncHook(),
      radioChanged: new SyncHook(["radioStation"])
    };
  }

  turnOn() {
    this.hooks.carStarted.call();
  }

  setRadioStation(radioStation) {
    this.hooks.radioChanged.call(radioStation);
  }
}

const myCar = new Car();
myCar.setRadioStation("100.10");

물론, 훅을 탭하고 있는 함수가 하나도 없다면 위 이야기는 모두 무의미한 것일 뿐입니다.

Hook에 Tap하기

어떤 훅을 탭하려면 tap 메서드를 사용하면 됩니다.

const myCar = new Car();
myCar.hooks.carStarted.tap("EngineLampPlugin", () => {
  console.log("Car started!");
});
myCar.hooks.radioChanged.tap("RadioPlugin", (radioStation) => {
  console.log(`Radio changed to ${radioStation}`);
});

myCar.turnOn();
// "Car started!"

myCar.setRadioStation("100.10");
// "Radio changed to 100.10"

첫번째 인자는 현재 훅에 탭하는 플러그인의 이름입니다. 이 이름은 이후 디버깅 시에 식별 정보로서 활용됩니다.
두번째 인자는 해당 훅이 발생하였을 때 실행할 콜백 함수를 전달합니다. 이 콜백 함수는 훅의 call 메서드로 전달된 인자들을 고스란히 사용할 수 있습니다.

앞서 다룬 클릭 이벤트 리스너를 떠올려보시면 좋습니다. "click" 이벤트가 발생하면 콜백이 실행됩니다. 이 콜백은 이벤트 객체를 사용할 수 있습니다. 플러그인 또한, call 메서드로 전달되는 인자들을 사용할 수 있게 됩니다.

역자 주: 원 저자는 Webpack 플러그인을 그저 함수 콜백 정도로 간주하고 있습니다. 플러그인이라고 부르니 거창해보이지만, 결국 Webpack의 번들링 생명 주기 내에서 실행되는 여러 함수 중 하나에 불과하죠. 위의 예시 코드를 예로 보면, .tap 메서드에 두번째 인자로 전달하는 콜백이 결국 플러그인의 본질인 것입니다.

Hook을 가로채기

Tapable로 생성된 훅과 언제 상호작용할 수 있는지 알아야 할 때도 있습니다. 훅의 생명 주기 도중 접근할 수 있는 방법이 크게 3가지 존재합니다.

Register Interceptor

이 유형의 인터셉터는 훅을 탭할 때 실행됩니다. 즉, 이 인터셉터는 단 한번만 실행됩니다. 인터셉터는 Tap 객체를 제공받는데, 이 객체에는 플러그인 이름, 해당 플러그인이 실행할 함수 등의 정보가 들어있습니다.

class Car {
  constructor() {
    this.hooks = {
      carStarted: new SyncHook()
    };

    this.hooks.carStarted.intercept({
        register: (tapInfo) => {
            console.log(`${tapInfo.name} is registered`);
            return tapInfo;
        }
    })
  }

  turnOn() {
    this.hooks.carStarted.call();
  }
}

const myCar = new Car();
myCar.hooks.carStarted.tap("EngineLampPlugin", () => {
  console.log("Car started!");
});
// EngineLampPlugin is registered
myCar.hooks.carStarted.tap("BluetoothPlugin", () => {
  console.log("Bluetooth enabled");
});
// BluetoothPlugin is registered

myCar.turnOn();
// Car started!
// Bluetooth enabled

register 내의 인터셉터 코드 상에서 Tap 객체를 수정할 수도 있습니다. 예를 들어, 플러그인이 실행할 함수를 오버라이드할 수도 있습니다.

class Car {
  constructor() {
    this.hooks = {
      carStarted: new SyncHook()
    };

    this.hooks.carStarted.intercept({
        register: (tapInfo) => {
            if (tapInfo.name === "NitroPlugin") {
                console.log(`🚫 ${tapInfo.name} is banned 🚫`);

                tapInfo.fn = () => {
                  console.log(`🚨 Police are on their way 🚨`);
                };
            } else {
                console.log(`${tapInfo.name} is registered`);
            }

            return tapInfo;
        }
    })
  }

  turnOn() {
    this.hooks.carStarted.call();
  }
}

const myCar = new Car();
myCar.hooks.carStarted.tap("EngineLampPlugin", () => {
  console.log("Car started!");
});
// EngineLampPlugin is registered
myCar.hooks.carStarted.tap("NitroPlugin", () => {
  console.log("🏎 lets go fast");
});
// 🚫 NitroPlugin is banned 🚫

myCar.turnOn();
// Car started!
// 🚨 Police are on their way 🚨

역자 주: carStarted훅에 대하여 NitroPlugin을 탭하면 "🏎 lets go fast" 메시지가 출력되어야 하겠지만, carStarted의 인터셉터에서 NitroPlugin의 콜백을 제한해버리고 있으므로 메시지 내용이 🚨 Police are on their way 🚨으로 고정되버립니다.

Call Interceptor

이 유형의 인터셉터는 훅이 발생할 때마다 실행됩니다. 이 인터셉터는 해당 플러그인이 사용할 수 있는 모든 인자를 제공받습니다.

class Car {
    constructor() {
      this.hooks = {
        radioChanged: new SyncHook(["radioStation"])
      };
  
      this.hooks.radioChanged.intercept({
        call: (radioStation) => {
            console.log("Looking for signal...");
            console.log(`Signal found for ${radioStation}`);
        }
      })
    }

    setRadioStation(radioStation) {
        this.hooks.radioChanged.call(radioStation);
    }
  }
  
  const myCar = new Car();
  myCar.hooks.radioChanged.tap("RadioPlugin", radioStation => {
    console.log("Station was changed", radioStation);
  });
  
  myCar.setRadioStation("100.1");
  // Looking for signal...
  // Signal found for 100.1
  // Station was changed 100.1

  myCar.setRadioStation("100.3");
  // Looking for signal...
  // Signal found for 100.3
  // Station was changed 100.3

Tap Interceptor

이 유형의 인터셉터는 훅이 호출될 때마다 실행됩니다. call 인터셉터와는 다른데, 왜냐하면 이 인터셉터는 훅에 탭한 모든 플러그인에 대하여 한번씩 전부 실행되기 때문입니다. 인터셉터는 Tap 객체를 제공받지만, Tap 객체를 수정할 수는 없습니다.

class Car {
    constructor() {
      this.hooks = {
        radioChanged: new SyncHook(["radioStation"])
      };
  
      this.hooks.radioChanged.intercept({
        tap: (tapInfo) => {
            console.log(`${tapInfo.name} is getting called`);
        }
      })
    }

    setRadioStation(radioStation) {
        this.hooks.radioChanged.call(radioStation);
    }
  }
  
  const myCar = new Car();
  myCar.hooks.radioChanged.tap("RadioPlugin", radioStation => {
    console.log("Station was changed", radioStation);
  });
  myCar.hooks.radioChanged.tap("SpeakerPlugin", radioStation => {
    console.log("Updating Speaker UI", radioStation);
  });
  
  myCar.setRadioStation("100.1");
  // RadioPlugin is getting called
  // Station was changed 100.1
  // SpeakerPlugin is getting called
  // Updating Speaker UI 100.1

  myCar.setRadioStation("100.3");
  // RadioPlugin is getting called
  // Station was changed 100.3
  // SpeakerPlugin is getting called
  // Updating Speaker UI 100.3

결론

Tapable Hook에 대한 간단한 소개글을 마칩니다. 훅을 사용하는 유용한 방법을 알고 계시다면 알려주시기 바랍니다.

0개의 댓글