본문으로 건너뛰기

HMR 에서 Hot Reloading 로

무료2020-05-31#Tool#hot-reloading#Hot Reload原理#react hot reload under the hood#how does hot reloading work#Hot Reload实现机制

Hot Reload 는 어떻게 실현되는가?

一.HMR

HMR(Hot Module Replacement) 은 실행 시의 JavaScript 모듈에 대해 핫 업데이트를 실행할 수 있습니다 (재로딩 불필요로, 모듈의 치환, 추가, 삭제가 가능)

(webpack HMR 에서 인용)

HMR 특성은 webpack 등의 구축 도구에 의해 제공되며, 애플리케이션 층 프레임워크 (React, Vue 등) 가对接하기 위한 일련의 런타임 API 를 공개합니다:

Basically it's just a way for modules to say "When a new version of some module I import is available, run a callback in my app so I can do something with it".

그 기본 원리는, 실행 시에 (구축 도구가 기동한) Dev Server 에 대해 폴링을 실행하고, script 태그를 통해 업데이트된 모듈을 실행 환경에 주입하며, 관련하는 콜백 함수를 실행하는 것입니다:

HMR is just a fancy way to poll the development server, inject <script> tags with the updated modules, and run a callback in your existing code.

예를 들어:

import printMe from './print.js';

if (module.hot) {
  module.hot.accept('./print.js', function() {
    console.log('Accepting the updated printMe module!');
    printMe();
  })
}

HMR 을 유효하게 하면, ./print.js 모듈에 업데이트가 있는 경우, 콜백 함수가 트리거되어, 모듈이 치환 완료한 것을 나타냅니다.그 후 해당 모듈에 액세스하면, 모두 새로운 모듈 인스턴스가 취득됩니다

실행 시의 모듈 치환 능력 (HMR) 에 기반하여, 애플리케이션 층 프레임워크 (React, Vue, 심지어 Express) 와 결합하여, Live Reloading, Hot Reloading 등의 보다 효율적인 개발 모드를 더욱 실현할 수 있습니다

二.Live Reloading

소위 Live Reloading 이란, 모듈 파일이 변화한 때에, 애플리케이션 전체를 재로딩하는 것입니다:

Live reloading reloads or refreshes the entire app when a file changes. For example, if you were four links deep into your navigation and saved a change, live reloading would restart the app and load the app back to the initial route.

React 를 예로:

const App = require('./App')
const React = require('react')
const ReactDOM = require('react-dom')

// Render the root component normally
const rootEl = document.getElementById('root')
ReactDOM.render(<App />, rootEl)

// Are we in development mode?
if (module.hot) {
  // Whenever a new version of App.js is available
  module.hot.accept('./App', function () {
    // Require the new version and render it instead
    const NextApp = require('./App')
    ReactDOM.render(<NextApp />, rootEl)
  })
}

HMR 을 통해 루트 컴포넌트를 치환하고, 재렌더링할 뿐 입니다.HMR 모듈 업데이트에는 버블링 메커니즘 이 있기 때문에, accept 로 처리되지 않는 업데이트 이벤트는 의존 체인을 역방향으로 전달합니다.따라서 컴포넌트 트리의 톱층에서 트리 중의 모든 컴포넌트의 변화를 감시할 수 있고, 이 때에 컴포넌트 트리 전체를 재작성하며, 프로세스 중에 취득하는 것은 모두 업데이트 완료한 컴포넌트이며, 렌더링하면 새로운 뷰가 얻어집니다

이 방안은 애플리케이션 층 프레임워크への의존이 적고 (re-render 부분のみ), 실장이 간단하고 안정 신뢰할 수 있지만, 此前的인 실행 상태는 모두 손실되며, SPA 등의 실행 시 상태가 많고 복잡한 장면에 대해 극히 불친절 합니다.리프레시 후에 다시 조작하지 않으면 이전의 뷰 스테이트로 돌아갈 수 없고, 개발 효율의 향상은 매우 제한적입니다

그렇다면, 실행 시의 상태 데이터를 보유하고, 변화한 뷰만을 리프레시하는 방법은 있을까요?

있습니다.Hot Reloading 입니다

三.Hot Reloading

하층도 마찬가지로 HMR 에 기반하지만, Hot Reloading 은 애플리케이션의 실행 상태를 보유할 수 있고, 변화한 부분에 대해서만 부분적인 리프레시를 실행합니다:

Hot reloading only refreshes the files that were changed without losing the state of the app. For example, if you were four links deep into your navigation and saved a change to some styling, the state would not change, but the new styles would appear on the page without having to navigate back to the page you are on because you would still be on the same page.

뷰의 부분적인 리프레시는, 전체 리프레시 후에 다시 이전의 상태로 돌아가기 위한 번잡한 조작을 면제하며, 진정으로 개발 효율을 향상시킵니다

하지만, 부분적인 리프레시는 컴포넌트 (심지어 컴포넌트의 일부) 의 핫 치환을 요구하며, 이는 실장상에不小的인 도전이 존재합니다 (정확성의 보장, 영향 범위의 축소, 에러의 피드백 등을 포함.상세는 My Wishlist for Hot Reloading 참조)

어떻게 컴포넌트를 동적으로 치환하는가?

HMR 치환 후의 새로운 모듈은, 실행 시로부터 보면 완전히 다른 2 개의 컴포넌트이며, 이하에 상당합니다:

function getMyComponent() {
  // script 태그를 통해, 같은 컴포넌트 코드를 재로드
  class MyComponent {}
  return MyComponent;
}

getMyComponent() === getMyComponent() // false

명확히 React 자신의 Diff 메커니즘 을 통해 무상 치환을 완료할 수 없습니다.따라서, JavaScript 언어로부터 가능성을 찾을 수밖에 없습니다

클래식한 React 컴포넌트는 [ES6 Class](/articles/class-es6 笔记 10/) 를 통해 정의됩니다:

class Foo extends Component {
  state = {
    clicked: false
  }
  handleClick = () => {
    console.log('Click happened');
    this.setState({ clicked: true });
  }
  render() {
    return <button onClick={this.handleClick}>{!this.state.clicked ? 'Click Me' : 'Clicked'}</button>;
  }
}

실행 시에 컴포넌트 클래스에 기반하여 일련의 컴포넌트 인스턴스를 작성하며, 它们는 render 라이프사이클 등의 프로토타입 메소드를 가지고, handleClick 등의 인스턴스 메소드, 및 state 등의 인스턴스 속성을 가집니다

프로토타입 메소드, 프로토타입 속성은 치환이 어렵지 않지만, 인스턴스 메소드와 인스턴스 속성을 치환하는 것은 그만큼 간단하지 않습니다.它们는 컴포넌트 트리 중에 단단히 싸여 있기 때문입니다

그 때문에, 어떤 사람은 매우 영리한 방법을 생각해냈습니다

四.React Hot Loader

React 에코시스템에 있어서, 현재 (2020/5/31) 가장 널리 응용되고 있는 Hot Reloading 방안은 여전히 RHL(React Hot Loader) 입니다:

Tweak React components in real time.

컴포넌트 메소드의 동적 치환을 실현하기 위해, RHL 은 React 컴포넌트 위에 1 층의 프록시를 추가 했습니다:

Proxies React components without unmounting or losing their state.

P.S.상세는 react-proxy 참조

중요 원리

1 층의 프록시를 통해 컴포넌트 상태를 박리하여, 프록시 컴포넌트 중에 배치하여 유지합니다 (其余의 라이프사이클 메소드 등은 모두 소스 컴포넌트에 프록시).따라서 소스 컴포넌트를 치환한 후에도 컴포넌트 상태를 보유할 수 있습니다:

The proxies hold the component's state and delegate the lifecycle methods to the actual components, which are the ones we hot reload.

소스 컴포넌트는 프록시 컴포넌트에 싸여, 컴포넌트 트리에 걸려 있는 것은 모두 프록시 컴포넌트입니다.핫 업데이트 전후로 컴포넌트 타입은 변화하지 않고 (배후의 소스 컴포넌트는 이미 슬그머니 새로운 컴포넌트 인스턴스에 치환되어 있습니다), 따라서 추가의 라이프사이클 (componentDidMount 등) 을 트리거하지 않습니다:

Proxy component types so that the types that React sees stay the same, but the actual implementations change to refer to the new underlying component type on every hot update.

구체적인 실장 상세는, 이하를 참조:

Redux Store

특별히, Redux 애플리케이션而言, Reducer 의 변화도 핫 유효하게 하는 것이 필요합니다 (대부분의 상태는 Redux 에 의해 관리되고 있기 때문에):

// configureStore.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducer from '../reducers';

export default function configureStore(initialState) {
  const store = createStore(
    reducer,
    initialState,
    applyMiddleware(thunk),
  );

  if (module.hot) {
    module.hot.accept(() => {
      const nextRootReducer = require('../reducers/index').default;
      store.replaceReducer(nextRootReducer);
    });
  }

  return store;
};

replaceReducer 를 통해 Reducer 를 치환하고, 동시에 store 상태를 보유합니다

P.S.Redux 애플리케이션 Hot Reloading 에 관한 상세 정보는, RFC: remove React Transform from examples 참조

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성