jineecode

일회성 초기화를 위한 useState 본문

JS/react

일회성 초기화를 위한 useState

지니코딩 2022. 8. 3. 11:14

앱의 수명당 한 번만 초기화하려는 리소스가 있다고 가정합니다. 

권장되는 패턴은 일반적으로 구성 요소 외부 에 인스턴스를 만드는 것 입니다.

이것은 일반적으로 redux store과 같이 앱 당 한 번 필요한 리소스에 적합합니다.

import type { AppProps } from 'next/app';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';



const queryClient = new QueryClient()
  
function MyApp({ Component, pageProps }: AppProps) {
  return (
    <QueryClientProvider client={queryClient}>
          <Component {...pageProps} />
    </QueryClientProvider>
  );
}

export default MyApp;

그러나 구성요소를 여러 번 마운트하는 경우, 각각에는 자체 리소스가 필요합니다. 그 중 두 개가 동일한 리소스를 공유하면 리소스를 컴포넌트 안으로 옮겨야 합니다.

 

import type { AppProps } from 'next/app';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';


function MyApp({ Component, pageProps }: AppProps) {
  // 🚨 be aware: new instance is created every render
  const queryClient = new QueryClient() // <--
  return (
    <QueryClientProvider client={queryClient}>
          <Component {...pageProps} />
    </QueryClientProvider>
  );
}

export default MyApp;

이제 렌더링 기능은 모든 렌더링에 대해 새 리소스를 생성합니다.

그러나 언제든 재렌더링이 발생할 수 있으므로 대비해야 합니다.

 

# 1. useMemo

useMemo는 종속성이 변경되는 경우에만 값을 다시 계산되는데 사용되며, 종속성이 없는 다음 코드는 괜찮아보입니다.

import type { AppProps } from 'next/app';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';


function MyApp({ Component, pageProps }: AppProps) {
  // 🚨 still not truly stable
  const queryClient = React.useMemo(() => new QueryClient(), [])
  return (
    <QueryClientProvider client={queryClient}>
          <Component {...pageProps} />
    </QueryClientProvider>
  );
}

export default MyApp;

그러나 공식문서는 useMemo가 성능최적화로 사용된다고 합니다. 

https://ko.reactjs.org/docs/hooks-reference.html#usememo

 

Hooks API Reference – React

A JavaScript library for building user interfaces

ko.reactjs.org

useMemo는 성능 최적화를 위해 사용할 수는 있지만 의미상으로 보장이 있다고 생각하지는 마세요. 가까운 미래에 React에서는, 이전 메모이제이션된 값들의 일부를 “잊어버리고” 다음 렌더링 시에 그것들을 재계산하는 방향을 택할지도 모르겠습니다. 예를 들면, 오프스크린 컴포넌트의 메모리를 해제하는 등이 있을 수 있습니다. useMemo를 사용하지 않고도 동작할 수 있도록 코드를 작성하고 그것을 추가하여 성능을 최적화하세요.

위 코드는 성능 문제가 아니라, 참조 안정성을 원하므로 useMemo를 선택하는 것은 옳지 않습니다.

 

# 2. useState

import type { AppProps } from 'next/app';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';


function MyApp({ Component, pageProps }: AppProps) {
  // ✅ truly stable
  const queryClient = React.useState(() => new QueryClient(), [])
  return (
    <QueryClientProvider client={queryClient}>
          <Component {...pageProps} />
    </QueryClientProvider>
  );
}

export default MyApp;

state는 setter를 호출하는 경우에만 업데이트 되도록 보장됩니다. 즉, setter를 호출하지 않고 튜벌의 첫 번째 부분만 사용한다면 한 번만 호출되게 할 수 있다.

 

# 3. useRef

useRef  사용하여 동일한 결과를 얻을 있다react 규칙에 따르면 이는 렌더링 기능의 순수성을 손상시키지도 않습니다.

더보기

useState 등을 이용하여 리액트가 직접 폼요소에 개입, 제어하는 방식을 'controlled component(제어 컴포넌트)' 라 하고,
리액트의 간섭을 최소화하고 HTML native 기능을 최대한 활용하는 방식을 'uncontrolled component(비제어 컴포넌트)'라 칭합니다.

제어 컴포넌트는 리액트가 폼요소의 상태변화를 실시간으로 감지하여 바로바로 반응(React)합니다.
예컨대 form 변경시마다 매번 react 코드로 validation check을 하고 싶다거나, form 변경사항을 즉시 다른 곳에도 영향을 주고자 할 때는 이런 제어컴포넌트가 적합합니다. 다만 이런 즉각성 때문에 성능저하는 불가피합니다.

한편 비제어 컴포넌트는 폼요소의 변경사항에 대해 리액트가 일절 관여하지 않습니다. 때문에 기본적으로 리액트의 성능에 영향을 주지 않습니다. 대신 순수 javascript api를 통해 필요한 시점에 리액트에게 정보를 '알려주는' 동작이 필요합니다.

폼요소의 '변경사항'을 즉시 알아야 하는 경우는 그다지 많지 않기 때문에 기본적으로는 비제어 컴포넌트가 더 좋다고 생각하지만, 요구사항에 따라 필요에 따라 두 방식을 취사선택할 수 있으면 더욱 좋겠죠 :)

import type { AppProps } from 'next/app';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';


function MyApp({ Component, pageProps }: AppProps) {
// ✅ also works, but meh
  const queryClient = React.useRef(null)
   if (!queryClient.current) {
    queryClient.current = new QueryClient()
   }
  
  return (
    <QueryClientProvider client={queryClient.current}>
          <Component {...pageProps} />
    </QueryClientProvider>
  );
}

export default MyApp;

이것은 다소 복잡해 보이고 TypeScript도 그것을 좋아하지 않을 것입니다. 왜냐하면 resource.current 는 기술적으로 null 일 수 있기 때문 입니다. 나는 이러한 경우 에 useState 를 선호합니다. 

(실제로 react-query v4.0은 if문으로 감싸고 있다 하더라도 QueryClient를 null값에 할당하지 못하도록 경고합니다.)

 

 

 

참조:

https://tkdodo.eu/blog/use-state-for-one-time-initializations

 

useState for one-time initializations

Why you shouldn't rely on useMemo for guaranteed referential stability but prefer useState instead

tkdodo.eu

 

'JS > react' 카테고리의 다른 글

Container/Presentational Pattern  (0) 2022.08.03
next에서의 env 설정  (0) 2021.11.29
밸리데이션 체크  (0) 2021.11.29
NEXT.JS Data Fetching  (0) 2021.11.24
Next.js - typescript tip  (0) 2021.11.22
Comments