akjfal

Nextjs dynamic logic(view nextjs source code) 본문

공부

Nextjs dynamic logic(view nextjs source code)

akjfal 2022. 6. 16. 04:13

dynamic function 실행

dynamic<P = {}>(
  dynamicOptions: DynamicOptions<P> | Loader<P>,
  options?: DynamicOptions<P>
)

dynamicOptions

type DynamicOptions<P = {}> = LoadableGeneratedOptions & {
  loading?: ({
    error,
    isLoading,
    pastDelay,
  }: {
    error?: Error | null
    isLoading?: boolean
    pastDelay?: boolean
    retry?: () => void
    timedOut?: boolean
  }) => JSX.Element | null
  loader?: Loader<P> | LoaderMap
  loadableGenerated?: LoadableGeneratedOptions
  ssr?: boolean
  suspense?: boolean
}

이것이다. 

이 중 첫번째인 LoadableGeneratedOptions는

type LoadableGeneratedOptions = {
  webpack?(): any
  modules?(): LoaderMap
}

이 타입이다.

webpack은 뭔지 모르겠다. 네이밍을 추측해봤을때 웹팩이 아닐까 싶다.

modeles는

결론적으로 Promise를 통해서 가져온 React 컴포넌트거나, default라는 변수에 들어가있는 React 컴포넌트다.

즉  LoadableGeneratedOptions는 webpack설정과 React.ComponentType을 promise나 객체에 담아서 가져오는 것을 의미한다.

React.ComponentType<T>클래스 컴포넌트 또는 상태 비 저장 기능 컴포넌트의 결합이다. 상위 컴포넌트 또는 기타 유틸리티와 같은 React 컴포넌트를 수신하거나 리턴하는 함수에 사용하려는 유형이다.

react에 들어가보면 type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P으로 선언되어 있는데, 내부에 다양한 타입들이 있어 길어질 것 같아 패스했다.

=> DynamicOptions는 React.ComponentType과 loading및 여러 옵션들 중 겹치는 값을 의미한다.

Loader의 경우 DynamicOptions의 loader에도 들어가 있는 타입으로 React.ComponentType을 의미한다.

==> dynamicOptions는 리엑트의 컴포넌트를 의미한다.

options의 경우에도 DynamicOptions하나만 선언되어져 있다. 하지만 ComponentType과 자체적으로 만든 객체들 중 겹치는 인자들만 가져가고 있으므로 약간의 차이가 있다.

==> options는 리엑트의 컴포넌트의 속성값들 중 일부를 의미한다.

※ ReactComponent자체에 ssr, suspense 속성들이 있는 것으로 추측되는 것으로 보아, 18버전으로 올라가면서 컴포넌트 자체에 SSR관련 설정드링 포함되어져 있는 것으로 추측된다.


첫 줄부터 또 지옥이다...

let loadableFn: LoadableFn<P> = Loadable

loadbleFn에는 Loadble을 변수에 저장한다. 이는 loadble.js에 따로 선언되어져있다. 추후 해당 변수를 사용할 때 설명하도록 한다.

  let loadableOptions: LoadableOptions<P> = {
    // A loading component is not required, so we default it
    loading: ({ error, isLoading, pastDelay }) => {
      if (!pastDelay) return null
      if (process.env.NODE_ENV === 'development') {
        if (isLoading) {
          return null
        }
        if (error) {
          return (
            <p>
              {error.message}
              <br />
              {error.stack}
            </p>
          )
        }
      }

      return null
    },
  }

loadbleOptions에 LoadbleOptions 타입인 변수가 선언되는데 해당 변수에는 loading을 재선언 해주고 있다.

해당 로딩의 경우 개발자 모드에서의 처리를 담당하고 있다. 추후 해당 변수를 사용할 때 다시 확인하도록 한다.


변수 선언이 끝나고, 조건문들을 지나간다.

  // Support for direct import(), eg: dynamic(import('../hello-world'))
  // Note that this is only kept for the edge case where someone is passing in a promise as first argument
  // The react-loadable babel plugin will turn dynamic(import('../hello-world')) into dynamic(() => import('../hello-world'))
  // To make sure we don't execute the import without rendering first
  if (dynamicOptions instanceof Promise) {
    loadableOptions.loader = () => dynamicOptions
    // Support for having import as a function, eg: dynamic(() => import('../hello-world'))
  } else if (typeof dynamicOptions === 'function') {
    loadableOptions.loader = dynamicOptions
    // Support for having first argument being options, eg: dynamic({loader: import('../hello-world')})
  } else if (typeof dynamicOptions === 'object') {
    loadableOptions = { ...loadableOptions, ...dynamicOptions }
  }
  • dynamic(import(...))를 하는 경우를 처리해준다.
  • 첫번째 인자를 promise로 전달하는 edge케이스에만 작동한다.
  • 바벨이 dynamic(import(...))를 dynamic(()=>import())로 변경해준다.
  1. 첫 렌더링에 import가 작동하지 않도록 하기 위해 dynamicOptions이 promise일 때 loadbleOptions.loader에 dynamicOptions를 함수로 저장한다.
  2. dynamicOptions이 함수인경우 loadbleOptions.loader에 바로 저장한다.
  3. dynamicOptions이 object(React Component)일 경우 loadbleOptions를 구조분해할당한다.

  // Support for passing options, eg: dynamic(import('../hello-world'), {loading: () => <p>Loading something</p>})
  loadableOptions = { ...loadableOptions, ...options }

options값들을 loadableOptions들에 구조분해할당 해주면서, 기존 로딩과 같은 기본 설정들을 덮어씌운다.


  // Error if Fizz rendering is not enabled and `suspense` option is set to true
  if (!process.env.__NEXT_REACT_ROOT && loadableOptions.suspense) {
    throw new Error(
      `Invalid suspense option usage in next/dynamic. Read more: https://nextjs.org/docs/messages/invalid-dynamic-suspense`
    )
  }

Fizz Rendering이 사용불가하면서, 'suspense' 옵션이 ture일 경우 에러를 띄워준다.

Fizz Rendering이 무슨뜻인지 알 수 없어 에러가 띄우는 내용만 적어 놓자면 "next를 사용하는데 React 18 이전 버전에서 <Suspense>는 불가능하다"이다.


  // coming from build/babel/plugins/react-loadable-plugin.js
  if (loadableOptions.loadableGenerated) {
    loadableOptions = {
      ...loadableOptions,
      ...loadableOptions.loadableGenerated,
    }
    delete loadableOptions.loadableGenerated
  }

리엑트 바벨 플러그인을 사용하면서 처리하는 옵션들을 처리해주는 것으로 추측된다. 위에 코드들을 통해서 loadbleGenreated 즉 webpack과 React.ComponentType 옵션들이 변경된 것으로 추측된다.



  // support for disabling server side rendering, eg: dynamic(import('../hello-world'), {ssr: false}).
  // skip `ssr` for suspense mode and opt-in React.lazy directly
  if (typeof loadableOptions.ssr === 'boolean' && !loadableOptions.suspense) {
    if (!loadableOptions.ssr) {
      delete loadableOptions.ssr
      return noSSR(loadableFn, loadableOptions)
    }
    delete loadableOptions.ssr
  }

주석만 읽어보자면 ssr: false옵션이 들어갔을 때를 지원하기 위한 옵션이다. 단 suspense는 false가 되어야 해당 동작이 발생한다. 

※ 만약 해당 옵션을 false로 할 경우 Suspense속 컴포넌트는 fallback에서 지정해준 html 이 처음에 넘어오게되고, true로 할 경우 해당 컴포넌트가 먼저 렌더링되어져서 넘어오게된다.

※ suspense가 true일 경우 해당 번들은 lazyload되고 아닐 경우 이전에 오게된다.

만약 ssr옵션이 false일 경우 noSSR이란 함수가 돌아가게 된다.

export function noSSR<P = {}>(
  LoadableInitializer: LoadableFn<P>,
  loadableOptions: DynamicOptions<P>
): React.ComponentType<P> {
  // Removing webpack and modules means react-loadable won't try preloading
  delete loadableOptions.webpack
  delete loadableOptions.modules

  // This check is necessary to prevent react-loadable from initializing on the server
  if (!isServerSide) {
    return LoadableInitializer(loadableOptions)
  }

  const Loading = loadableOptions.loading!
  // This will only be rendered on the server side
  return () => (
    <Loading error={null} isLoading pastDelay={false} timedOut={false} />
  )
}

처음에 선언된 2개의 변수가 인자로 들어오게되는데

우선 loadbleOptions에 있는 modules(컴포넌트)와 webpack을 삭제한다. 해당 코드를 통해서 react-loadble은 먼저 로드를 시도하지 않는다. 이 코드로 인해서 번들이 따로 로드되는 여부가 결정되는 것으로 추측된다.

다음으로 isServerSide의 변수가 만약 false라면 loadbleFn을 실행하는데 해당 함수는 서버를 초기화하는 코드라고 주석에 적혀있다. 즉 webpack과 modules가 삭제된 상태로 초기화해준다. 만약 없다면 loadableFn을 통해서 컴포넌트를 생성한 값을 넘겨준다.

다음으로 loadbaleOptions.loading!라는 코드가 있는데 여기에서 !는 typescript에서 무조건 값이 할당되어져 있는것으로 간주한다라는 뜻이다.

이러한 작동으로 인해서 options.loading으로 넘어간 컴포넌트가 서버에서 렌더링 되게 된다.


이러한 과정들을 거치고 (만약 ssr:false, suspense: false가 아닌경우에) loadbleFn이 실행된다.

우선 2가지 경우(1. webpack과 modules가 제거된 경우, 2. 남아 있는 경우) 2가지로 나뉘어서 loadableOptions가 넘어간다는 것을 기억하자.

function Loadable(opts) {
  return createLoadableComponent(load, opts)
}

초기 Loadble실행이다. 우선 createLoadbleComponent의 결과값을 리턴해준다는 것을 알 수 있는데 해당 리턴값이 dynamic의 리턴값과 동일하다. 그리고 인자로 load와 loadableOptions이 넘어간다.

createLoadbleComponent로 들어가보자

function createLoadableComponent(loadFn, options) {
  let opts = Object.assign(
    {
      loader: null,
      loading: null,
      delay: 200,
      timeout: null,
      webpack: null,
      modules: null,
      suspense: false,
    },
    options
  )

loadFn(load) 객체이며, options(loadableOptions)가 인자로 넘어오게 된다.

 

  if (opts.suspense) {
    opts.lazy = React.lazy(opts.loader)
  }

만약 suspense 옵션이 true로 설정된다면 컴포넌트는 React.lazy를 통해서 불러와진 결과값이 opts.lazy에 저장된다. 즉 suspense라는 옵션에 대한 처리가 여기에서 이루어진다.

  // Server only
  if (typeof window === 'undefined') {
    ALL_INITIALIZERS.push(init)
  }

window가 없다면 init의 결과값이 ALL_INITIALIZERS(초기값 : [])에 push 된다.

  /** @type LoadableSubscription */
  let subscription = null
  function init() {
    if (!subscription) {
      const sub = new LoadableSubscription(loadFn, opts)
      subscription = {
        getCurrentValue: sub.getCurrentValue.bind(sub),
        subscribe: sub.subscribe.bind(sub),
        retry: sub.retry.bind(sub),
        promise: sub.promise.bind(sub),
      }
    }
    return subscription.promise()
  }

init은 subscription이 null일때 조건문이 먼저 돈다.(맨 처음 코드가 작동할 경우 이러한 현상이 발생할 것으로 추측된다) 

이때 loadFn(loadableOptions)이며, opts는 모든 값이 초기값으로 되어있는 객체다.

class LoadableSubscription {
  constructor(loadFn, opts) {
    this._loadFn = loadFn
    this._opts = opts
    this._callbacks = new Set()
    this._delay = null
    this._timeout = null

    this.retry()
  }

해당 객체를 생성한다. 

생성하면서 this.retry가 작동하는데

  retry() {
    this._clearTimeouts()
    this._res = this._loadFn(this._opts.loader)

    this._state = {
      pastDelay: false,
      timedOut: false,
    }

    const { _res: res, _opts: opts } = this

    if (res.loading) {
      if (typeof opts.delay === 'number') {
        if (opts.delay === 0) {
          this._state.pastDelay = true
        } else {
          this._delay = setTimeout(() => {
            this._update({
              pastDelay: true,
            })
          }, opts.delay)
        }
      }

      if (typeof opts.timeout === 'number') {
        this._timeout = setTimeout(() => {
          this._update({ timedOut: true })
        }, opts.timeout)
      }
    }

    this._res.promise
      .then(() => {
        this._update({})
        this._clearTimeouts()
      })
      .catch((_err) => {
        this._update({})
        this._clearTimeouts()
      })
    this._update({})
  }

1. this._clearTimeouts가 먼저 동작하면서

  _clearTimeouts() {
    clearTimeout(this._delay)
    clearTimeout(this._timeout)
  }

clearTimeout을 통해 this.delay와 this.timeout을 제거한다.

2. this._loadFn(loadableOptions)에 this._opts.loader를 넣어준다. 이를 통해 dynamicOptions에 넣었던 import(...)로 불러온 결과값이 저장이 된다. 이 값이 this._res(import(...) 의 결과)에 저장된다.

    this._state = {
      pastDelay: false,
      timedOut: false,
    }

3. this._state에 pastDelay와 timeOut이 false로 저장된다.(loading에 옵션으로 넣었던 변수이름임)

    const { _res: res, _opts: opts } = this

    if (res.loading) {
      if (typeof opts.delay === 'number') {
        if (opts.delay === 0) {
          this._state.pastDelay = true
        } else {
          this._delay = setTimeout(() => {
            this._update({
              pastDelay: true,
            })
          }, opts.delay)
        }
      }

      if (typeof opts.timeout === 'number') {
        this._timeout = setTimeout(() => {
          this._update({ timedOut: true })
        }, opts.timeout)
      }
    }

4. 불러오기 도중일때 처리를 해준다. 우선 delaytimeout이 남아있을 때다.

  • delay
    • 0이라면 pastDelay를 true로 해준다.
    • 시간이 남아있다면 _dealy에 setTimeout을 선언하는데 delay 이후 업데이트 함수를 통해서 error, loaded, loading(import 문의 진행상황) 상태 중 this._statepastDelay를 true로 해주며 callback문들을 실행해준다.
    • 즉 딜레이가 끝난 이후나 0일 경우 pastDelay를 true로 변경해준다는 의미다.
  • timeout
    • timeout이 남아있을 땐 this._statetimeOut을 true로 해준다.
  _update(partial) {
    this._state = {
      ...this._state,
      error: this._res.error,
      loaded: this._res.loaded,
      loading: this._res.loading,
      ...partial,
    }
    this._callbacks.forEach((callback) => callback())
  }

5. this._res.promise(load에서 리턴값으로 넘어온 state의 promise) 후 처리를 진행해준다.

    this._res.promise
      .then(() => {
        this._update({})
        this._clearTimeouts()
      })
      .catch((_err) => {
        this._update({})
        this._clearTimeouts()
      })
    this._update({})

이렇게 class의 constructor가 끝났다.

이제 subscription변수와 sub를 바인딩 해주는데 

      subscription = {
        getCurrentValue: sub.getCurrentValue.bind(sub),
        subscribe: sub.subscribe.bind(sub),
        retry: sub.retry.bind(sub),
        promise: sub.promise.bind(sub),
      }

getCurrentValue는 this._state

subscribe는 callback들을 추가해주기

retry는 위에서 설명

promise는 this._res.promise를 의미한다.

이렇게 바인딩된 subscription의 this._res.promise를 리턴해준다.

    return subscription.promise()

=> init을 통해서 import문의 결과(React Component)를 리턴해준다.

이렇게 서버만 있을때 초기 값을 처리했다.


 

다음은 클라이언트만 있을 때다.

  // Client only
  if (!initialized && typeof window !== 'undefined') {
    // require.resolveWeak check is needed for environments that don't have it available like Jest
    const moduleIds =
      opts.webpack && typeof require.resolveWeak === 'function'
        ? opts.webpack()
        : opts.modules

처음에 initialized는 false로 초기화 되어 있기 때문에 무조건 false이며, window는 서버와 동일하게 아직 설정되지 않았을 것이다.

webpack이 존재하고 resovleWeak를 사용할 수 있다면 webpack을 사용하고, 없다면 modules를 사용한다.(noSSR의 경우 webpack을 삭제했으므로 modeles를 사용할 것으로 예상된다)

    if (moduleIds) {
      READY_INITIALIZERS.push((ids) => {
        for (const moduleId of moduleIds) {
          if (ids.indexOf(moduleId) !== -1) {
            return init()
          }
        }
      })
    }
  }

noSSR이 아닌 경우에 READY_INITIALIZERS(초기값 [])에 해당 함수를 넣어주게 된다.(ids의 경우 READY_INITIALIZERS의 길에 따라 몇번째 index가 되는지가 리턴된다)

※ 해당 부분은 webpack과 modules에 어떠한 값이 들어가는지 정확히 몰라 파악 불가...


  const LoadableComponent = opts.suspense ? LazyImpl : LoadableImpl
  LoadableComponent.preload = () => init()
  LoadableComponent.displayName = 'LoadableComponent'

다음으로 해당 함수들이 도는데 suspense의 선언에 따라 분기처리가 된다.

1. LazyImpl

  function LazyImpl(props, ref) {
    useLoadableModule()

    return React.createElement(opts.lazy, { ...props, ref })
  }
  function useLoadableModule() {
    init()

    const context = React.useContext(LoadableContext)
    if (context && Array.isArray(opts.modules)) {
      opts.modules.forEach((moduleName) => {
        context(moduleName)
      })
    }
  }

modules에 값이 들어가 있다면, 모듈값이 들어가게된다. 이를 통해 modules은 해당 값을 React.useContext를 통해 관리 되는 값인 것을 알 수 있다.

다음 React.lazy를 통해 불러온 값들을 통해 컴포넌트를 생성한다.

2. LoadableImpl

  function LoadableImpl(props, ref) {
    useLoadableModule()

    const state = useSyncExternalStore(
      subscription.subscribe,
      subscription.getCurrentValue,
      subscription.getCurrentValue
    )

    React.useImperativeHandle(
      ref,
      () => ({
        retry: subscription.retry,
      }),
      []
    )

    return React.useMemo(() => {
      if (state.loading || state.error) {
        return React.createElement(opts.loading, {
          isLoading: state.loading,
          pastDelay: state.pastDelay,
          timedOut: state.timedOut,
          error: state.error,
          retry: subscription.retry,
        })
      } else if (state.loaded) {
        return React.createElement(resolve(state.loaded), props)
      } else {
        return null
      }
    }, [props, state])
  }

동일하게 useLoadbleModule을 통해 컴포넌트와 context를 생성한다. 

이후 useSyncExternalStore를 통해 state를 생성한다.

useSyncExternalStore : 외부 데이터를 가져와주는 싱크를 맞춰준다.

다음 useImperativeHandle을 통해서 부모 컴포넌트와 연결시킨다.

결과로 useSyncExternalStore로 만들어진 state에 따라서 만들어진 컴포넌트를 리턴해준다.

=> 즉 suspense에 따라 useSyncExternalStore로 생성된 컴포넌트를 리턴해주냐 처리 안된 컴포넌트를 생성해주냐라는 차이점이 발생한다. 즉 동기 렌더링을 진행하냐 마냐의 차이점이 발생한다.

'공부' 카테고리의 다른 글

React18과 Next.js(정리)  (0) 2022.06.13
24일차  (0) 2022.06.02
22일차  (0) 2022.05.30
21일차  (0) 2022.05.23
20일차  (0) 2022.05.15
Comments