기막힌 Streaming SSR을 사용하고 싶습니다. (실패)
NextJs 버전을 보고 결정하자. 13.1.1 에서는 dynamic을 사용해서 CSR과정에서 suspense를 사용하는 것으로 해결함.
의식의 흐름대로 쓴 글입니다
회원가입 과정에서, 이메일 인증하는 부분을 개발하던 중이었다.
토큰을 검증하고 유저의 이메일을 프론트에서 받아오는 과정에서 문제가 생겼다.
조금 더 정확히 말하면, 화면을 렌더링하는 시점에 email 값이 없는 문제였다.
다음과 같은 오류가 발생해서 문제를 해결해야만 하는 상황.
사실 optional chaining
을 사용하여, 아래처럼 바꿔주면 문제는 금방 사라진다.
return <SignupFormView email={data?.email} />
이럴수는 없잖아요...
근데 아무리 생각을 해봐도,optional chaning
을 사용하지 않는 경우에도 정상적으로 동작을 했어야 한다.
왜냐하면 부모 컴포넌트에 Suspense
를 사용하고 있기 때문.
다음과 같은 구조이다.
<Suspense fallback={<Spinner/>}>
<SignupForm/>
</Suspense>
Suspense를 사용했기 때문에, 다음과 같은 흐름을 예상했다.
하지만 어림도 없는 상황.
export const useVerifyTokenQuery = () => {
...
return useQuery<Token>(
queryKeys.auth.emailToken(token),
() => verifyEmailToken(token),
{
suspense: true, // 줬는데?
},
);
};
어... 안된다. 근데 오류 메시지가 다르네?
그리고 next dev server 콘솔에 request log가 찍히네?
아니다. 만약에 백엔드쪽 문제였으면 data?.email 했을때도 오류가 났어야했는데, 잘만 동작했음.
고민을 계속 하다가, 렌더링 방식에 문제가 있는건가 싶어서, 렌더링에 관련된 몇 가지 문서들을 봤다.
리액트 18에서 말하는 주요 특징들은, 정리 잘된 글들이 너무 많다.
따라서 특징을 설명하기 보다, 내가 겪은 문제에 어떻게 적용할 수 있는지에 집중해보고자 한다.
Nextjs 는 기본적으로 모든 page에 대해 pre-rendering
을 한다.
따라서 Signup
페이지도 pre-rendering을 할 것.
문제는 pre-rendering과정에서 react-query 혹은 async api call이 어떻게 동작하는가?
라는 결론에 도달했다.
공식 문서에 2가지 방식이 잘 나와있다.
1. Using initialData (채택 x)
getServerSideProps
로 api를 호출하고, 그 값을 페이지에 주입하는 방식이다.
// signup.tsx (page)
const Signup = (props) => (
<SignupForms email={props.email} />
);
export default Signup;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const token = context.query.token;
const result = await verifyEmailToken(token);
//console.log(result)
return {
props: {
email: result.email,
},
};
}
// SingupForms (component)
const SignupForms = ({ email }) => {
const { data } = useVerifyTokenQuery(email);
return <SignupFormsView email={data.email} />;
};
콘솔을 찍어보면 터미널에 결과도 원하는 대로 출력된다.
몇 가지 trade off도 발생한다.
useVerifyTokenQuery
를 호출 하는 경우에, initialData를 같이 props로 전달해줘야 한다. (prop drilling)useVerifyTokenQuery
를 사용하는 경우에 initialData를 항상 같이 넘겨주어야 한다.useVerifyTokenQuery
가 언제 fetch 되었는지 알 수 없기 때문에, load time을 기준으로 refetch를 해야한다.개인적으로는 page단에서 props를 만들어서 넘겨주는 것을 선호하지 않는다.
컴포넌트 내에서 모든 것을 처리하고, page단에서는 "어떤 컴포넌트로 구성되어있구나~" 정도만 확인할 수 있었으면 좋겠다고 생각하기 때문에, 1번 방법을 채택하지 않았다.
(모든 api를 page 단에서 관리하여, props로 넘겨주는 방식을 선호하지 않다고는 하지만, 사실 컴포넌트의 depth가 깊어지면 말짱 도루묵이긴 하다. 저도 알고싶지 않았어요)
2. Using Hydration (안해봄)
시도 해보고 다시 작성해보겠다. (진짜 할거임)
leblanc's Law : 나중은 결코 오지 않는다
오~ 그러면 이제 헤더는 곧바로 나오고, 로딩바가 뜨겠지?
라는 부푼 기대를 가지고, 실험을 하나 해봤다.
setTimeout
를 이용해서, 10초 뒤에 이메일이 포함된 화면이 뜨도록 코드를 작성해봤다.
const email = await new Promise((resolve, _) => {
setTimeout(() => {
resolve("inkyu0103@naver.com");
}, 10000);
});
내가 기대했던 것과 달리, 10초 뒤에 전체 화면이 나오더라.
분명 Next 13에서 된다고 했던 것 같은데...?
이걸 못봤네
Next의 app directory를 사용하지 않으면, Streaming SSR을 쓸 수 없는 것 같아보인다.
사실 email 하나만 받아오면 되는거라, 무거운 api를 호출하는 것도 아니고 server side에서 미리 렌더링을 할 필요가 있을까 싶었다.
그래서 그냥 getServerSideProps 이런거 다 없앴다.
그리고 CSR을 하는 과정에서 Suspense를 사용하기 위해, Suspense를 Custom해서 사용하는 방식을 선택했다.
지금 같은 상황은 여러 코드를 작성해서 가독성을 떨어뜨리는 것보다,
아래와 같이 직관적으로 코드를 작성하는 것이 낫지 않을까 생각해본다.
const Signup = () => (
<SSRSuspense fallback={<Spinner />}>
<SignupForms />
</SSRSuspense>
);
글을 쓰다가, React.lazy() 를 사용하는 것과 Next가 제공하는 dynamic
의 결과가 다른 것을 발견했다.
dynamic 사용 시 - 중간에 잠깐 하얀 화면이 보였다가 회원가입 form이 나옴
React.lazy() 사용 시 - 의도한 대로 동작
공식문서에는 다 답이 있었다.
dynamic은 Suspense를 포함하고 있고, ssr을 사용할 것인지 여부를 설정 할 수 있다.
const SignupForms = dynamic(() => import("components/forms/SignupForms"), {
loading: () => <Spinner />,
ssr: false,
});
이렇게 작성해주면, 아래와 같이 한 줄만 써줘도 csr에서 suspense를 동작시킬 수 있다. 야호!
const Signup = () => <SignupForms />;