React19가 아직도 공식적으로 release되지 않은 데에는 많은 이유가 있겠지만,
그 중 하나는 Suspense로 인한 개발자들의 열띤 토론에서도 나온다고 느낍니다.
이 글에서는...
- React19에서 Suspense가 어떻게 변경되는 지에 대해 이야기합니다.
- 해외 개발자들의 깃허브(순한맛)와 레딧(매운맛) 반응을 공유합니다.
React19가 출시됨을 알린 4월 25일의 트윗에, Gabriel Valfridsson 씨가 답글을 남깁니다.
"캬 ~~ 훌륭한 변경사항이 1톤이 넘는구만요!!!"
"근데 이건 좀 더 주의를 강조해야 할 것 같은데 ... 링크
"리액트 쿼리같은 라이브러리와 Suspense를 사용하면 옛날엔 병렬 로딩이 됐었는데 이제는 워터폴이 생기네용? 링크 1 링크 2
예제를 축약해보면 다음과 같습니다.
import { useSuspenseQuery } from "@tanstack/react-query";
import { Suspense, useState } from "react";
export default function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<Suspense fallback="Loading...">
<Thing1 />
<Thing2 />
</Suspense>
);
}
function Thing1() {
const query = useSuspenseQuery({ queryKey: ["thing1"], ... });
return <div>{query.data}</div>;
}
function Thing2() {
const query = useSuspenseQuery({ queryKey: ["thing2"], ... });
return <div>{query.data}</div>;
}
React18에서의 Suspense를 다음과 같이 쓸 때는 병렬적으로 렌더링이 진행됐었습니다.
그래서 만약 Thing1과 Thing2의 suspense 작업이 둘 다 1초가 걸린다고 가정하면,
병렬적으로 처리되어 1초 후에 Loading fallback이 풀리고 결과물이 렌더링됩니다.
허나 React19에서는 동일한 가정에서 2초 후에 결과물이 렌더링된다는 것을 알 수 있습니다.
Thing1이 처리된 다음, Thing2가 처리되는 식으로 동작하기에 2초가 걸리게 되는 겁니다.
변경되는 이유는 기존의 Suspense가 어떻게 동작하는지와도 관련이 있습니다.
이유에 대해 리액트 팀의 결론부터 말하자면 불필요한 렌더링을 줄이기 위함입니다.
Suspense를 사용하면 데이터 로딩이 끝난 후 렌더링을 시작하는 게 아니라,
데이터 로딩을 시작하는 동시에 렌더링을 시작할 수 있게 됩니다.
export default function App() {
return (
<Suspense fallback={<p>...</p>}>
<SuspendingComponent />
<ExpensiveComponent />
</Suspense>
)
}
예제 출처 : 이춘구님의 "[번역] React 19와 Suspense - 3막극" (감사합니다)
SuspendingComponent가 비동기 데이터를 로딩하는 컴포넌트이고,
ExpensiveComponent가 렌더링 비용이 큰 컴포넌트라고 가정해보겠습니다.
여기서 상황을 정리해보자면,
이 과정에서 suspend되는 요소로 인해 사전에 필요 없는 렌더링이 발생해서,
리액트 팀은 이를 오버헤드로 판단하고 React19에서는 워터폴을 만들기로 결정한 것입니다.
개발자들이 반발하는 이유는 일단 첫 번째로 모든 어플리케이션이 위와 같은 상황을 거치지 않습니다.
그리고 워터폴로 바꾸면 렌더링이 너무 느려지게 됩니다.
2초의 suspend를 요구하는 20개의 컴포넌트가 하나의 Suspense내에 있다고 하면,
원래 2초가 걸릴 렌더링이 사전 렌더링 불필요하다고 2*20 -> 40초가 걸리게 됩니다.
리액트 팀의 입장은 어떨까요?
압도적인 싫어요 수... 요약하자면
"애초에 Suspense lazy loading 하라고 만든거라 fetch랑 쓰는 건 부적합한데용?"
"Suspense는 이제 라우터 단에서 prerender할 때만 쓰세여."
"Suspense로 fetch 시작하지 말고 그냥 리소스를 소비하는 용도로만 쓰시고여."
이런 이유에서 개발자들이 여러 discussion이나 커뮤니티에서 열띤 토론을 펼치고 있습니다.
어쩌면 React19가 아직도 정식 출시되지 않은 이유는 이 때문일 지도 모르겠네요...
"제가 데이터를 가져오는 가장 좋아하는 방법은 데이터를 사용하는 곳에 가까이 두는 방법이에요." (캡슐화한 각 컴포넌트에서의 데이터 fetch를 이야기하시는 듯함)
"Suspense를 변경하기 전까지는 가능했어요. 이렇게 되면 각 컴포넌트마다 하나씩, 총 20개나 있는 fetch를 전부 다 컴포넌트의 최상위로 끌어올려서 써야되잖아여..."
"님들 React.lazy 때문에 '리액트'를 이루는 성배가 깨진듯 함..."
"워터폴로 렌더링해야 하는 개발자가 있다고 생각하면 일단 Suspense 옛날처럼 되돌려주고 선택 가능한 옵션처럼 만들어주세용..."
"아니 당신네들 이거보고도 계속 그 이야기할 수 있는거요?"
"Suspense에 의존하는 React Query랑 더불어 Three.js는 본질적으로 비동기로 움직이니까 이런 플랫폼 전부에 영향을 미치잖소"<Center> <AsyncThingA /> <AsyncThingB /> </Center>
"위 코드에서 Center는 AsyncThingA와 AsyncThingB가 완료되었을 때를 확인 후 useLayoutEffect에서 그에 따른 처리를 할 수 있었는데, React19에서는 워터폴로 만들어버리면 TTL이 증가하잖아..."
"그럼 이제 Center를 동적이거나 조건적으로 작동시킬 수 없고 만약에 이게 라이브러리라서 고치지도 못하면 해결할 방법이 읎잖소!"<Suspense revealOrder={"forwards" | "backwards" | "together"} fallback={...}> <Foo /> <Bar /> </Suspense>
"그렇게 바꾸고 싶으면 차라리 이런 opt-in 넣어서 선택할 수 있게 하든가 하쇼"
필자의 주관적인 생각으로는 Suspense를 바꾼다면 선택할 수 있는 opt-in으로 바꾸면
호불호가 갈리지 않을텐데, 상황을 조금 더 지켜보아야 할 필요가 있는 듯 합니다.
"React가 이상한 점은 이런 상황에서 opt-in같은걸 안넣는다는 거임.. 핵심적인 기능을 조용하게 바꾸는 건 미친 짓 아님?"
"어 Vercel이 React가 Next에 의존하게 만들어서 Next 많이 쓰게 하고 돈 뜯어 묵을라고 그럼 ㅋㅋ"
"좀 지나친 발언이라고 말할려고 했는데... Suspense를 Vercel이 contribution했네요? ㅋㅋㅋ"
"버셀이 PR 올리고 자기가 approve했어요 '놀 랍 다!'"
"기여하려면 최소 다른 두 개의 조직이 PR을 approve해야 한다고 생각해요.
필자 또한 어떤 이유로 인해 Suspense의 동작이 극단적으로 변경되는지는 아직 잘 모르고 있으나,
이런 기능들이 선택할 수 있는 opt-in으로 출시되면 많은 개발자들의 환호를 받을 수 있을 것이라고 생각합니다.
특히나 Suspense를 lazy loading의 관점으로 변경한 contributor가 prerender를 권유하고 있는 Next.js(Vercel)라면, 레딧에서 개발자들이 조금 과격하게 추측할 여지가 있을 것 같기도 합니다.
React19가 정식적으로 출시되지 않는 상태에서 Next15가 정식 출시된 것도 이상하긴 한데,
경과를 지켜보아야 할 것 같습니다.
저도 19에서 출시되는 use나 useOptimistic을 보고 환호했는데,
이런 이슈가 있었다는 것은 최근에 알게 된 사실이네요.
React19를 쓰신다면 꼭 변경되는 Suspense의 차이점을 숙지하고
사용하시는 것을 추천드립니다.
글 작성 시점 이후(2024/11/15)에 나온 React 19 RC1에서는 해당 이슈를 Suspense의 fallback을 렌더하고 보내준 후에 다른 노드를 (non-blocking하게) 렌더해서, React <=18의 렌더가 오래 걸리는 컴포넌트가 Suspense fallback 렌더를 늦추는 문제와 React 19 RC0의 waterfall이 생기는 문제 둘 모두 해결한 듯합니다.
https://react.dev/blog/2024/04/25/react-19-upgrade-guide#improvements-to-suspense