리엑트 Suspense 딥다이브

dante Yoon·2022년 11월 12일
59

react

목록 보기
16/19

동영상 강의

https://www.youtube.com/watch?v=uz_xLiyR0BA

글을 쓰기에 앞서서

안녕하세요, 단테입니다.

리엑트18 버전부터 StrictMode 에서 useEffect가 기본적으로 두번씩 호출 됨에 따라 useEffect는 api를 호출하기 더욱 적합하지 않은 장소가 되었습니다.

react query, swr과 같은 캐싱 기능을 기본적으로 제공하는 라이브러리들의 등장으로 인해 useEffect를 사용하지 않아도 쉽게 컴포넌트 레벨에서 api 응답을 받아볼 수 있게 되었습니다.

이에 더 나아가 최근 해외 리엑트 관련 컨퍼런스들에서는 render as you fetch 라는 패턴으로 각 컴포넌트의 뷰를 그려주는 방식을 많이 소개 하고 있습니다.

render as you fetch 기법을 사용하기 위해서는 리엑트의 Suspense를 잘 활용해야 하며 잘 이해하고 있으면 컴포넌트를 작성하며 가장 많이 마주하는 비동기 처리에 대해 좀 더 나은 코드들을 작성할 수 있습니다.
이것이 오늘 Suspense를 설명하기 전 api 호출에 대한 이야기를 먼저 꺼낸 이유입니다.

아무쪼록 이번 포스팅을 읽어보실 동기가 되었으면 좋겠으며 본격적으로 suspense에 대한 이야기를 시작하겠습니다.

Suspense 발전 과정

코드 스플리팅 v16.6

리엑트 Suspense는 코드 스플리팅을 위해 등장한 컴포넌트입니다.

We introduced a limited version of Suspense several years ago. However, the only supported use case was code splitting with React.lazy, and it wasn’t supported at all when rendering on the server. - React v18.0

16버전부터 React.lazy와 연계해 사용해왔습니다.

import React, { lazy, Suspense } from 'react';

const AvatarComponent = lazy(() => import('./AvatarComponent'));

const renderLoader = () => <p>Loading</p>;

const DetailsComponent = () => (
  <Suspense fallback={renderLoader()}>
    <AvatarComponent />
  </Suspense>
)

원본 코드: https://glitch.com/edit/#!/react-lazy-suspense
구동 예시: https://react-lazy-suspense.glitch.me

코드 스플리팅은 네트워크를 통해 받아오는 JS 청크를 앱을 구동하는데 필요한 만큼만 다운로드 받아올 수 있게 잘게 나누는 기능입니다.

공식 문서에서는 v16.6 에서 처음 공개되었습니다.

공식 석상에서는 JSConf 2018 Island에서 처음 소개되었습니다.

당시 Suspense는 아래와 같은 한계점을 가지고 있었는데요,

  • 서버사이드 렌더링에서는 Suspense를 지원하지 않았습니다.
  • Suspense는 컴포넌트 이름에서 드러나듯이 렌더링 하기 전에 특정 작업을 기다리는 것입니다. Suspense가 처음 소개된 이후 마이너 업데이트를 통해 Apollo, Relay를 이용해 data fetching 전 fallback UI를 보여주는 용도로 사용할 수 있었지만 캐싱 지원의 불안정함으로 인해 권장하지 않았습니다.

v16.6에서 lazy loading을 위한 Suspense feature가 처음 공개된 이후 Suspense는 계속 보완되고 있습니다.

Server side rendering support v18

서버사이드 렌더링이 산업 전반적으로 필수 아닌 필수이자 하나의 트렌드로 되어가며 CSR에서 사용되던 리엑트 api들도 동일하게 SSR환경에서 지원되어야 했는데요, react/server api의 renderToString과 같이 SSR에서 사용되는 api들은 Suspense를 전혀 지원하지 않았기 때문에 이 부분이 큰 불편함이었습니다.

사용하지 않거나 loadable-components와 같은 써드파티 라이브러리를 사용해야 했습니다.

v18에서는 서버에서도 Suspense 사용이 가능하게 되었습니다.

SSR 지원에 더해서 Streaming 기능이 추가되었는데 정확히 어떤점이 변경되었는지 자세하게 알고 싶으신 분은 네이버 FE news 2021 7월호에 수록된 포스팅을 참고해보세요.

Data fetching

Render as you fetch

이미 많은 분들이 Suspense를 사용해 비동기 처리를 하고 있습니다. 대개 라이브러리의 도움을 받는데요, 아직 data fetching을 위한 api 구현 및 캐싱 전략이 완성되지 않았기 때문에 각 라이브러리들도 실험적이라는 미명 아래 지원하고 있습니다.

  • react-query는 Suspense와 useQuery를 함께 사용할 시 water-fall 문제를 야기할 수 있으며 useQueries 사용 시 Suspense를 지원하지 않습니다.

  • apollo client는 최근 SSR + Suspense 지원을 위한 RFC가 이슈에 생성되었습니다. (본 포스팅 작성 13일 전)

명시적으로 공식 문서에서 지원한다고 밝힌 라이브러리들을 보면 Relay, Next.js, Hydrogen, or Remix이 있습니다.
data fetching 라이브러리라고 하기 보다는 full stack framework가 많이 보입니다.

이는 현재로서는 당연한 과정일지도 모르겠습니다.
리엑트 코어 팀에서도 아직 명확한 가이드를 제시하고 있지 않기 때문입니다.

  • render as you fetch를 사용하면 기존 useEffect를 사용할 때보다 코드가 매우 간단해지며,
  • 라이브러리에서 제공하는 캐시 기능을 사용할 시 useEffect를 사용했을 때 고려해야 할 race condition을 피할 수 있고, 불필요한 api 호출을 하지 않을 수 있다는 장점이 있습니다.

다음은 리엑트 공식문서에서 제공해주는 예제 코드입니다.
두 ProfileDetails, ProfileTimeLine 컴포넌트는 컴포넌트 렌더링 시 user, post 정보를 호출합니다.

const resource = fetchProfileData();

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // Try to read user info, although it might not have loaded yet
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  // Try to read posts, although they might not have loaded yet
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );

각 컴포넌트에서 데이터를 사용하기 위해 하는 일에 대해 주목해보세요.

그저 데이터를 읽는 것밖에 하는 것이 없습니다.

데이터를 읽어올 때동안 해주어야할 기존 작업들은 모두 이 두 컴포넌트를 사용하는 상위 레벨에서 역할을 담당하게 됩니다.

실제로 예제 코드를 변경하여 실행하고 싶으시다면 아래 코드 샌드박스에 접속해서 확인해보세요.

Suspense를 사용하지 않았을 때의 코드는 어떤 형태일까요?

fetch then render

function ProfileDetails() {
  const [loading, setLoading] = useState(true);
  const [user, setUser] = useState();
  // Try to read user info, although it might not have loaded yet
  useEffect(() => {
    const abortController = new AbortController();
    const { signal } = abortController;
    fetchUser({ signal }).then((user) => {
      setLoading(false);
      setUser(user);
    });
    return () => {
      abortController.abort();
    };
  }, []);

  return <h1>{loading ? "....loading" : user.name}</h1>;
}

loading state를 표현해주고 데이터를 호출하며 호출한 데이터에 따라 뷰를 업데이트시켜주기 위해 상태 값을 두 개 선언했습니다.

race condition을 막기 위해 abortController를 선언하며, 예제 코드에서는 나와있지 않지만 fetchUser는 자체적으로 signal을 받아들일 수 있도록 내부적으로 구현되어야 합니다.

코드가 복잡해진다, 그래요 참을 수 있는 문제입니다.

race condition이 발생한다, 그래요 abortController를 써서 해결했습니다.

그런데 아직 해결하지 못한 문제가 있습니다.

React Advanced London 2022 - Day1

water fall problem

리엑트의 컴포넌트는 부모에서 자식으로 내려오며 순차적으로 렌더링됩니다. 생명주기 api인 useEffect 내부에서 데이터를 호출한다면 dom mount 이후 호출되기 때문에 렌더링 직후 데이터를 호출하기 시작합니다.

상위 컴포넌트의 렌더링이 끝나지 않는다면 계층 구조 깊숙히 있는 하위 컴포넌트는 데이터를 호출하는 시점이 그만큼 지연됩니다.

Suspense를 사용하며 컴포넌트 외부에서 데이터를 호출하게 된다면 해당 코드가 선언된 모듈 파일이 실행되는 시점에 맞춰서 api 호출이 시작되고, loading state에 대한 관리가 필요 없어집니다.

컴포넌트 밖에서 fetch를 했을 때 어떤 형태로 나타나는지 살펴보죠.

Remix

import { useLoaderData } from "@remix-run/react"
import { json } from "@remix-run/node";
import { getItems } from "../api";

export const loader = async () => {
  const items = await getItems();
  
  return json(items);
}

export default function Store() {
  const items = useLoaderData();
}

NextJS

import {getItems} from "../api";
function Store({items}) {
   ...
}
   
export async function getServerSideProps() {
  const items = await getItems();
  
  return {props: {items} }
}
  
export default Store;

React-Query

const issuesQuery = { queryKey: ['issues'], queryFn: fetchIssues }

// ⬇️ initiate a fetch before the component renders
queryClient.prefetchQuery(issuesQuery)

function Issues() {
  const issues = useQuery(issuesQuery)
}

https://tkdodo.eu/blog/seeding-the-query-cache#prefetching

zustand

import create from 'zustand'

const useFishStore = create((set) => ({
  fishies: {},
  fetch: async (pond, signal) => {
    const response = await fetch(pond, {signal})
    set({ fishies: await response.json() })
  },
}))


// start fetching initially
useFishStore.getState().fetch(...)

data fetching을 훅 내부에서만 진행해야 한다는 고정관념을 버리고 파일 실행 시 fetch를 일찍, 그리고 렌더링 지연 시 보여줘야 할 fallback ui 표기는 Suspense에게 위임하는 것이 코드 구성과 UX에 더 유리하다고 생각합니다.

Suspense를 사용하게 하려면 어떻게 코드를 구성해야 하나요?

Suspense는 데이터를 호출하는 주체는 아닙니다. 저희가 api를 호출할 때 사용하는 라이브러리들과 리엑트 컴포넌트간의 렌더링이 지연되어야 하는 시점을 소통하는 하나의 매커니즘입니다.

앞서 맨 처음 봤었던 공식 문서의 예제 코드를 뜯어보면 react-query나 swr 같은 data fetching library를 별도로 사용하지 않았는데요, 어떤 식으로 Suspense를 사용하게끔 만들었는지 한번 보겠습니다.

export function fetchProfileData() {
  let userPromise = fetchUser();
  let postsPromise = fetchPosts();
  return {
    user: wrapPromise(userPromise),
    posts: wrapPromise(postsPromise)
  };
}

// Suspense integrations like Relay implement
// a contract like this to integrate with React.
// Real implementations can be significantly more complex.
// Don't copy-paste this into your project!
function wrapPromise(promise) {
  let status = "pending";
  let result;
  let suspender = promise.then(
    (r) => {
      status = "success";
      result = r;
    },
    (e) => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}

export const fetchUser = () => {
  console.log("fetch user...");
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("fetched user");
      resolve({
        name: "Ringo Starr"
      });
    }, 1000);
  });
};

기본적인 아이디어는 이렇습니다. fech, axios와 같은 http 라이브러리의 호출을 promise로 한번 감싸고 응답이 오기 전까지는 계속 이 promise를 throw해줍니다.

wrapPromise 함수의 반환부를 살펴보세요.

...
read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
...

본 코드는 StrictMode를 적용하지 않았습니다.

function ProfileTimeline() {
  
  // Try to read posts, although they might not have loaded yet
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}

Suspense는 자식 컴포넌트를 마운트 시켰을 때 지연되어야 작업이 있다면 언마운트 시키고 fallback ui를 보여줍니다.

여기서 지연 작업은 data fetching 뿐만이 아닌 스크립트, 정적 파일 로딩도 포함됩니다.

그리고 작업이 완료되면 컴포넌트를 다시 렌더링해줍니다.

와우! 그럼 이제 라이브러리 없어도 저희가 직접 wrapping 해서 쓰면 되겠네요.

좋은 방법은 아닙니다. 왜냐하면 저희가 아직 Suspense의 내부 구현을 정확히 모르기 때문입니다.

아래 코드가 어떻게 동작할지 예상해보세요.

export const fetchDitto = () => {
  return new Promise(resolve => {
    console.log("hi")
    setTimeout(() => {
      resolve([{메타몽: "메타메타"},null])
    }, 3000)
  })
}

export const fetchType = () => {
  console.log("fetchPika")
  return new Promise(resolve => {
    setTimeout(() => {
      resolve([{피카츄: "피카피카"},null])
    },5000)
  })
}


const fetchDittoData = () => {
  const dittoPromise = fetchDitto();
  return wrapPromise(dittoPromise);
}

const fetchPikaData = () => {
  const pikaPromise = fetchType();
  return wrapPromise(pikaPromise)
}

export const Ditto = () => {
  const [ditto, error] = fetchDittoData().read();
  const [pika, _] = fetchPikaData().read()
  return (
    // <TestThrowPromsie/>
    <div>
      <div>
        ditto: {JSON.stringify(ditto)  }
        {error? `ditto error: ${JSON.stringify(error)}` : null}
      </div>
    </div>
  )
}

나눠서 볼게요. Ditto 컴포넌트 부터 보죠.

위에 정의한 fetchDittoDate의 반환 값인 wrapPromise를 받아 read하고 있습니다 . 여기서 wrapPromise는 앞서 봤던 예제코드의 wrapPromise와 동일합니다.

export function wrapPromise(promise: Promise<any>) {
  let status = "pending";
  let result: any;
  let suspender = promise.then(
    (r) => {
      status = "success";
      result = r;
    },
    (e) => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}

Promise가 pending 상태일 때는 pending 상태의 promise를 그대로 throw 해주고, fulfilled 상태일 때는 결과 값을 반환해줍니다.

Dito 컴포넌트에서는 wrapPromise의 반환 값을 컴포넌트 내부에서 호출하고 있습니다.

어떻게 동작하는지 콘솔을 한번 볼까요?

hi가 연속적으로 찍히고 있습니다.

실제로 코드를 동작시켜보면 3초에 한번씩 찍히는데요, fetchPika는 찍히지 않습니다.

컴포넌트 내부에서 fulfilled, pending 상태인 Promise를 throw 하면 리렌더링 됩니다.
throw된 위치에서 리렌더링이 되기 때문에 fetchPika는 콘솔에 출력되지 않은 것입니다.

코드를 살짝 변경해볼게요.

export const Ditto = () => {
  console.log("re-render")
  const [ditto, error] = fetchDittoData().read();
  console.log("how about me?")
  return (

    // <TestThrowPromsie/>
    <div>
      <div>
        ditto: {JSON.stringify(ditto)  }
        {error? `ditto error: ${JSON.stringify(error)}` : null}
      </div>
    </div>
  )
}

how about me?는 찍히지 않습니다.

Ditto 컴포넌트를 Suspense로 감싸면 우리가 의도한대로 동작할까요?

export const VanillaApp = () => {
  return (
    <div>
      바닐라 앱
      <Suspense fallback="야생의 메타몽이 나타났다...!">
        <Ditto/>
      </Suspense>
    </div>
  )
}

fallback ui에서 더 이상 진도를 나가지 않습니다.

아니요, 사실 리렌더링이 아닙니다.

근데 진짜 리렌더링일까요? re-render 이라는 글자가 console.log를 통해 출력된다고 모두 리렌더링이라고 생각하면 안됩니다.

왜나면 컴포넌트가 언마운트 되고 다시 마운트 된 것일 수도 있기 때문입니다.

아래 컴포넌트는 3초에 한번씩 카운터를 변경해줍니다. 상태 변경으로 인해 컴포넌트가 3초에 한번씩 리렌더링을 하는 것입니다.

export const Counter = () => {
  const [count,setCount] = useState(0);
  const ref = useRef(Math.ceil(Math.random() * 1000));
  console.log(ref.current)
  useEffect(()=> {
    setTimeout(() => {
      setCount(prev => ++prev)
    },3000)
  },[count])

  return <>{count}</>
}

ref 값은 리렌더링이 되더라도 변경되지 않기 때문에 어떻게 출력되는지 확인해보겠습니다.

이제 ref를 동일하게 Ditto 컴포넌트에 선언하고 출력해보겠습니다.


export const Ditto = () => {
  const ref = useRef(Math.ceil(Math.random() * 1000));
  console.log(ref.current);
  console.log("re-render")

  const [ditto, error] = fetchDittoData().read();

  console.log("how about me?")
  return (
    // <TestThrowPromsie/>
    <div>
      <div>
        ditto: {JSON.stringify(ditto)  }
        {error? `ditto error: ${JSON.stringify(error)}` : null}
      </div>
    </div>
  )
}

아하! 리렌더링 되는게 아니었습니다. 기존 컴포넌트가 언마운트되고 새로운 컴포넌트가 마운트되는 것이었습니다.

컴포넌트가 언마운트 되고 다시 마운트될 때마다. 새로운 promise 객체를 만들어 throw 하는 것입니다.

따라서 우리는 Promise fulfilled, pending 상태의 객체가 컴포넌트 내부에서 throw 되면 unmount, remount를 한다는 것을 알게되었습니다.

계속 pending과 fulfilled를 반복했기 때문에 fallback ui를 보여주는 것 이상의 진도를 나가지 못한 것입니다.

<Ditto /> 컴포넌트를 감싸는 Suspens를 없애면 상위 컴포넌트 중 가장 가까운 에러 처리 컴포넌트에 해당하는 ui가 보여지게 됩니다.

우린 여기서 다음과 같이 유추해볼 수 있습니다.

Suspense는 가장 가까운 자식 컴포넌트에서 throw되는 Promise의 상태 값에 따라 fallback ui를 보여준다.

근데 되게 이상하네요. 왜냐면 앞서서 공식 문서에서 동일한 wrapPromise를 사용했는데 제대로 동작했었거든요.

그러네요. 공식문서 코드를 다시 봅시다.


const resource = fetchProfileData();

function ProfilePage() {
  return (
    <Suspense
      fallback={<h1>Loading profile...</h1>}
    >
      <ProfileDetails />
      <Suspense
        fallback={<h1>Loading

Ditto 컴포넌트와는 확연하게 다른 부분이 있습니다. 컴포넌트 트리 외부에서 promise 객체를 instantiate 했다는 점입니다.

컴포넌트에서 참조되는 promise 객체가 동일하기 때문에 반복된 언마운트, 마운트가 일어나지 않는 것입니다.

Suspense를 사용하지 않아도 발견할 수 있는 현상입니다.

useMemo를 사용하면요?

const fetchedPromise = useMemo(() => fetchDittoData(),[])
const [ditto, error] = fetchedPromise.read();

ref를 사용하면요?

const fetchRef = useRef(fetchDittoData())

동일해요. 리렌더링이 아니라 컴포넌트가 UI트리에서 없어졌다가 다시 생기는 것이기 떄문입니다.
컴포넌트 언마운트, 마운트와 관계없이 동일한 객체 참조를 유지하지 않으면 무한루프에 빠지게 됩니다.
data fetch를 하기 전 컴포넌트 내부에서만 참조해야 하는 콘텍스트가 있을 경우 해결하기 어려운 부분이기 때문에 직접 구현하는 것은 어려울 수 있습니다.

컴포넌트 외부에서 Promise를 생성하면 해결되는 것이 아닌가요?

이 질문에 대한 모범답안이 아직 없습니다.

외부 라이브러리와 Suspense 통합을 위한 구체적인 가이드가 아직 나오지 않은 상태입니다.

Suspense 자세한 내부 구현에 대해 공개된 내용도 아직 없습니다.

약 한달 전에 나온 RFC use를 통해 리엑트 자체적인 data fetching + cache api 구현에 대한 논의가 한창 진행되고 있습니다.

useEffect는 앞으로 많은 곳에서 useSyncExternalStore, useEvent, use 훅으로 대체될 것이라고 예상합니다.

suspense-in-react-18 rfc

suspense-in-react-18 rfc PR

사소한 질문일 수 있는데, 현재 상황에서 컴포넌트가 suspend 할 수 있게 하는 일반적인 방법은 무엇인가요?
'The excat protocol a component should use to tell React that it's suspense'를 제가 잘못 해석한 것일 수 있지만 리엑트에게 컴포넌트가 나는 현재 지연되었다라는 것을 말할 수 있는 방법은 무엇입니까라는 의문점이 있다는 말로 받아들였거든요

정확합니다. 현재로서 작동 가능한 방법은 Promise를 throw 하는 것입니다. 하지만 이것은 공식적인 방법은 아니며 단순히 임의의 Promise객체를 throw 한다고 의도에 맞게 동작하지는 않습니다. 렌더링 시 마다 Promise객체가 생성되는 것을 막기 위해 캐시를 해야 하는데 이것을 직접 구현하기는 어려움이 있습니다.

위의 대화를 보면 알겠지만 Data fetching과 관련되어서는 항상 캐시를 어떻게 할 것인가에 대한 문제가 남습니다.

컴포넌트를 단순히 suspend 된 상태라는 것을 리엑트에게 알리는 것에서 더 나아가, water fall problem을 막기 위해 과거에 Data fetching을 한 결과값을 컴포넌트에서 조회할 수 있게 해야 하고 언제 invalidation을 할지 등등에 대한 구현 과제가 일반 유저에게 남는 것입니다.

캐시처리는 대단히 난이도가 있는 주제입니다.

난이도가 있으니 우리는 리엑트 쿼리를 사용하는 것이죠.

리엑트쿼리 왜 쓰는지는 아세요?

컴포넌트 내부에서 Promise 객체를 만들어서 throw 했을 때 발생할 수 있는 water fall 문제

아직까지 Suspense와 data fetching을 사용할 떄 어떻게 구현해야 하는지에 대한 가이드가 나오지 않았기 때문에 Suspense를 결합해서 사용하는 라이브러리에서 관련 문제가 발생하기도 합니다.

react-query에서는 컴포넌트 내부에서 useQuery를 여러개 호출 시 data fetching이 병렬적으로 일어나는 것이 아닌,

순차적으로 일어나는 문제가 있습니다.

https://github.com/TanStack/query/issues/3911

리엑트 메인테이너 Tkdodo 씨가 작성한 최근의 블로그를 보면 이에 대한 문제점과 해결 방안에 대해 말하고 있는데요,

컴포넌트에서 query를 연달아 호출 할 때

const issues = useQuery({ queryKey: ['issues'], queryFn: fetchIssues })
const labels = useQuery({ queryKey: ['labels'], queryFn: fetchLabels })

https://tkdodo.eu/blog/seeding-the-query-cache#suspense
the example codes and images are scrapped from above link blog post


위와 같은 water fall 문제는 suspense 옵션을 함께 사용했을 때 발생합니다. 따라서 render as you fetch 패턴을 현재 외부 라이브러리와 사용 시 fetch 함수를 호출 / 캐시를 읽는 동작이 컴포넌트 외부에 선언되었는지, 내부에 선언되었는지 파악해볼 필요가 있습니다.

글을 마치며

오늘은 리엑트 suspense api의 역사와 현재에 대해 알아보았습니다.

리엑트에서 built in cache관련 api가 어떻게 공개될지, 그 이후 외부 라이브러리들은 어떻게 통합 과정을 거칠지 개인적으로 궁금합니다.

Suspense, built-in cache, use api에 대한 추가 정보가 생기면 관련 포스팅을 추가로 발행하겠습니다.

감사합니다.

profile
성장을 향한 작은 몸부림의 흔적들

7개의 댓글

comment-user-thumbnail
2022년 11월 12일

금일 react-query useQueries 가 suspense 모드를 지원하게끔 수정되었습니다. https://github.com/TanStack/query/pull/4498

답글 달기
comment-user-thumbnail
2022년 11월 22일

정성있는 글 감사합니다!!

1개의 답글
comment-user-thumbnail
2022년 12월 5일

Suspense는 자식 컴포넌트를 마운트 시켰을 때 지연되어야 작업이 있다면 언마운트 시키고 fallback ui를 보여줍니다.

언마운트는 아닙니다 (useEffect return 이 불리지 않음)
(https://codesandbox.io/s/concurrent-mode-suspense-playground-forked-9z1k7c?file=/src/index.js)

https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md#behavior-change-committed-trees-are-always-consistent

1개의 답글
comment-user-thumbnail
2023년 1월 25일

오... 좋은 글 감사합니다!

답글 달기
comment-user-thumbnail
2023년 6월 14일

HyThe spouts have a special one-way valve that prevents contamination even after opening the box. Mountain water bottled water is safer than bottled water. The spout on the white label water is made of polyethylene - the same material found in private spring water.

답글 달기