ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [2019.08.27] 오늘의 TIL - Next.js + TypeScript + Mobx 사용시 주의사항 [Provider, inject]
    개발 블로깅/Next.js 2019. 8. 27. 23:51

     

    오늘 Next.js + TypeScript로 작업한 내용에 Mobx를 적용시켜서 새롭게 리펙토링을 하는 작업을 했다. 

    Mobx를 적용하면서 부딪혔던 이슈 사항들을 정리해보려고 한다. 

     

    # [Next.js + Mobx] 다른 pages로 렌더링 할 때 

    Next.js에서 최상위 컴포넌트는 pages디렉토리의 _app.js이므로, 해당 파일에 Provider를 적용시킨다.

    import App, { Container } from "next/app";
    import React from "react";
    import { Provider } from "mobx-react";
    import RootStore from "../stores";
    
    interface State {
      Store: RootStore;
    }
    
    export default class MyApp extends App<{}, State> {
    
      render() {  
        const store = new RootStore();
        return (
          <Provider {...store}>
            <Container>
              <Component {...pageProps} />
            </Container>
          </Provider>
        );
      }
    }
    

     

    이렇게 적용하면 첫 페이지에서는 제대로 동작한다.

    그러나 이후에 다른 페이지로 렌더링이 되는 순간, 아래와 같은 에러 메세지가 발생한다.

     

    MobX Provider: The set of provided stores has changed. Please avoid changing stores as the change might not propagate to all children

    에러 메세지와 같이, provider에 적용한 store가 변경되었다고 나온다. 

    새로운 페이지로 넘어갈 때, 최상위 페이지인 _app.js부터 렌더링 된 후 해당되는 페이지로 넘어가게 된다.

    render() {  
        const store = new RootStore();
        return (
          <Provider {...store}>
            <Container>
              <Component {...pageProps} />
            </Container>
          </Provider>
        );
      }

    위 코드와 같이, _app.js파일이 새로 렌더링 되면 Store를 다시 생성하여 provider에 적용하게 되어있다.

    Store를 새로 적용하게 되면, 기존에 관리되던 모든 State 상태가 날아가게 되므로, Store를 새롭게 적용하지 못하도록 되어있다.

     

    그러므로 _app.js에서 새로 렌더링이 돼도 Store가 변하지 않도록, 아래 코드와 같이 사용한다.

    import App, { Container } from "next/app";
    import React from "react";
    import { Provider } from "mobx-react";
    import RootStore from "../stores";
    
    interface State {
      Store: RootStore;
    }
    
    export default class MyApp extends App<{}, State> {
      state: State = {
        Store: new RootStore()
      };
      render() {
        const { Component, pageProps } = this.props;
    
        return (
          <Provider {...this.state.Store}>
            <Container>
              <Component {...pageProps} />
            </Container>
          </Provider>
        );
      }
    }
    

     

    _app.js도 하나의 컴포넌트이다. 

    Store를 State로 관리하면, 처음 렌더링 되었을 때만 새로 생성하고, 이후에는 리 렌더링 시에도 기존 Store를 사용할 수 있다.

     

    # [TypeScript + Mobx] inject로 Store를 가져올 때

     

    일반적인 inject 사용법은 아래와 같다.

    import React from "react";
    import { inject, observer } from "mobx-react";
    import tempStore from "../../stores/temp";
    
    @inject("tempStore")
    @observer
    class Counter extends React.Component {
    	render() {
          const {tempStore} = this.props;
          return (
            ...
          )
        }
    }
    
    

    global State를 props로 내려받도록 되어있다. (mobx뿐 아니라 Redux도 이와 같음)

    여기서 TypeScript를 적용 시, props에 대한 interface를 정의 해주어야 한다. (비록 global State를 가져오기 위한 props지만, TypeScript 특성 상 정의를 해주어야 함!)

    import React from "react";
    import { inject, observer } from "mobx-react";
    import tempStore from "../../stores/temp";
    
    interface Props {
      tempStore: tempStore; // error : Property 'tempStore' is missing in type '{}' but required in type 'Readonly<Props>'
    }
    
    @inject("tempStore")
    @observer
    class Counter extends React.Component {
    	render() {
          const {tempStore} = this.props;
          return (
            ...
          )
        }
    }

    위 코드처럼 Props에 대한 interface를 정의해버리면, TypeScript 특성상, Counter 컴포넌트를 불러오는 상위 컴포넌트가 tempStore라는 props를 내려주어야 한다.

    그러나 이 tempStore Props는 global State를 가져오기 위한 컴포넌트일 뿐이므로, 그냥 불러오게 되면 위 코드와 같은 에러 메세지를 보여준다.

    tempStore 컴포넌트에 내려줄 Props 타입이 빠졌다고 알려주지만, 상위 컴포넌트에서 내려줄 수 있는 값이 존재하지 않는다.

     

    이럴 땐, interface 정의를 아래와 같이 수정한다.

    interface Props {
      tempStore?: tempStore;
    }
    

    중간에 물음표(?)를 붙이게 되면, 해당 props는 존재할 수도 있고 없을 수도 있다는 의미가 되므로, 꼭 props를 안 내려 받아도 에러가 발생하지 않게 된다.

     

    그러나 이렇게 사용하면, 실제 tempStore의 State 값을 사용하려 할 때, 또 다른 에러메세지를 보여주게 된다.

    import React from "react";
    import { inject, observer } from "mobx-react";
    import tempStore from "../../stores/temp";
    
    interface Props {
      tempStore?: tempStore;
    }
    
    @inject("tempStore")
    @observer
    class Counter extends React.Component {
    	render() {
          const {tempStore} = this.props;
          return (
            <div>
              {tempStore} // error : Object is possibly 'undefined'
            </div>
          )
        }
    }

    tempStore를 직접 사용하려 하니, 해당 object가 undefined일 가능성이 있다고 에러메세지를 던진다. (tempStore를 물음표로 설정해 놓았기 때문!)

    TypeScript 특성으로 인해, 정확히 존재하는지 모르는 데이터 사용을 막는 것이다.

     

    이럴 때는 존재가 불명확한 변수 뒤에 느낌표(!)를 붙이면, 해당 변수가 존재하는 것으로 강제로 알리게 된다.

    render() {
          const {tempStore} = this.props;
          return (
            <div>
              {tempStore!} 
            </div>
          )
        }

     

    이렇게 하면, 이상없이 global State를 가져오는 컴포넌트를 어디서든 사용할 수 있다.

     

    번외)

    inject는 아래와도 같이 사용 가능하다.

    
    @inject(store => ({
      tempStore: store.tempStore
    }))
    @observer
    class Counter extends React.Component {
    	render() {
          const {tempStore} = this.props;
          return (
            <div>
              {tempStore} // error : Object is possibly 'undefined'
            </div>
          )
        }
    }

    이렇게 props로 가져올 store 값을 직접 명시해서, 여러 개의 global State를 props로 가져올 수 있다.

    반응형

    댓글

Designed by Tistory.