설모의 기록
[Mobx] observer class 의 render 함수 사용 본문
너무 오랜만에 글을 써서 티스토리 에디터가 변했는지도 몰랐네요..😭😭
최근 한 리엑트 프로젝트에 mobx 를 적용하다가 사소한 습관으로 인해 삽질한 경험이 있어 글을 쓰게 되었습니다. (빠른 시일 내에 React + Mobx + Typescript 적용기를 정리할 수 있길 바라며..)
mobx 의 기본 튜토리얼은 여기 를 보시면 됩니다 :)
코드
기본 튜토리얼을 보며 익히기 위해 복붙을 하지 않고 직접 작성을 했었습니다. 이게 삽질을 초래한 결과였죠.. (아래의 예제는 최대한 간단한 코드로 구성해보았습니다.)
아래의 코드는 React + Mobx + Typescript 로 구성되어 있습니다.
1. RootStore.ts
import { observable, action } from 'mobx';
export default class RootStore {
@observable name: string;
constructor() {
this.name = '';
}
@action
setSampleName(name: string) {
console.log('setSampleName'); this.name = name;
}
}
2. index.tsx (webpack 의 entry 파일로 설정됨)
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from 'App';
import { Provider } from 'mobx-react';
import RootStore from 'stores/RootStore';
ReactDOM.render(
<Provider rootStore={new RootStore()} >
<App />
</Provider>,
document.getElementById('root')
);
3. App.tsx
import * as React from 'react';
import { inject, observer } from 'mobx-react';
import RootStore from 'stores/RootStore';
interface AppProps {
rootStore?: RootStore;
}
@inject('rootStore')
@observer
class App extends React.Component<AppProps, {}> {
componentDidMount() {
console.log('componentDidMount');
const rootStore = this.props.rootStore as RootStore;
rootStore.setSampleName('this is blog named 설모의 기록');
}
render = () => {
const name = (this.props.rootStore as RootStore).name;
return (
<div
style={{
color: 'white',
background: 'blue',
padding: '20px',
fontSize: '16px',
textAlign: 'center'
}}
>hello, {name}</div>
);
}
}
export default App;
이렇게 하면 "hello, this is blog named 설모의 기록" 이 화면에 띄워질거라 생각하며 프로젝트 빌드를 돌렸습니다.
결과는...
name 값으로 빈 string 이 출력됩니다. 개발자 도구에서 콘솔을 확인해보면 componentDidMount 와 setSampleName 두 개 모두 출력되는데 말이죠..😭
무엇을 잘못한건지 몰라 mobx 패키지 버전 확인도 해보고 다시 설치도 해보고 @action 어노테이션 이외에도 runInAction() 등 여러가지 시도를 해봤습니다. 그러다가 예제와 다른 점을 찾게 되었는데 그것은 App 컴포넌트의 render 함수 사용이었습니다.
차이를 아시겠죠?? 저는 익명함수를 사용했고, mobx 는 멤버함수를 사용했습니다.
저 역시 멤버함수로 render 함수를 작성했더니 결과는 아래와 같이 원하던 결과가 출력되었습니다.
그렇다면 이 사소한 (그렇지만 사소하지 않은) 차이는 무엇일까요?
render = () => { } 와 render () { } 의 차이
javascript class 에서 두 함수를 어떻게 다루는지에 대한 차이점이었습니다.
우선 타입스크립트에서 자바스크립트로 어떻게 변환되는지 확인하기 위해 타입스크립트 플레이그라운드 에서 샘플 클래스를 작성해보았습니다.
차이점이 보이시나요?
render1 은 멤버함수로, render2 는 멤버변수가 되었습니다. 게다가 render2 는 생성자에서 초기화되고 있습니다.
다음은 클래스 변수를 살펴보겠습니다.
개발자 도구에서 위와 같이 코드를 작성한 후에 Sample 의 프로토타입을 살펴보면 render1 함수는 prototype 에 존재하는 반면에 render2 함수는 존재하지 않습니다.
이번엔 클래스를 통해 객체를 생성해보겠습니다.
sample 이라는 객체를 선언한 후 확인해보면 render2 라는 멤버변수 (키 값) 을 가지고 있지만 render1 은 멤버변수로 가지고 있지 않습니다. 그 대신 객체의 __proto__ (부모인 Sample 의 프로토타입) 에 저장되어 있습니다. 결국 render1 은 인스턴스화하기 전에 클래스의 프로토타입에 존재하지만 render2 는 인스턴스화하기 전에는 존재하지 않는것이죠.
이게 Mobx 와 무슨 상관?
이제 제가 했던 실수에 대한 이야기로 넘어오겠습니다.
함수를 익명함수로 작성하는 습관을 가지고 있다보니 예제를 따라해보면서 render 함수를 익명함수로 작성했습니다.
mobx 의 @observer 어노테이션은 관찰하는 데이터가 변경되면 컴포넌트의 shouldComponentUpdate() 메소드를 호출합니다. shouldComponentUpdate() 메소드를 클래스에 구현하지 않았더라도 mobx 내부적으로 shouldComponentUpdate() 메소드를 상속받도록 구현되어 있습니다.
컴포넌트의 shouldComponentUpdate() 가 호출되면 render() 함수가 호출됩니다. 그런데 render 함수를 익명함수로 작성해둔다면, 클래스를 인스턴스화하기 전에는 해당 메소드가 프로토타입에 존재하지 않습니다. 따라서 @observer 어노테이션을 붙인다 하더라도 그들이 실행할 render 함수가 없기 때문에 props 데이터가 변경되더라도 re-render 가 되지 않아 rootStore 의 name 이 계속 빈 string 을 출력하고 있었습니다. 'mobx-react' 의 observer 클래스 코드 를 봐도 render 함수를 가져와 사용하고 있습니다.
여기까지 삽질할만한(?) 내용 공유드립니다.
참고사이트
- https://medium.com/workday-engineering/react-performance-and-mobx-b038085ecb72