양파같은 서버사이드렌더링 이슈 해결기



TL;DR

문제 상황

  • 상품 상세페이지, 엑스퍼트 메인 등 마켓의 seo가 전반적으로 고장나 있었음.
  • authprovider 하위에서 서버사이드 렌더링이 작동하지 않고, 컴포넌트가 클라이언트에서만 실행됨. 우리는 NextSeo라는 seo를 위한 라이브러리를 사용중이다. 하지만 AuthProvider 하위에서는 NextSeo 컴포넌트가 렌더링 되지 않았다.

해결 시도


이슈1. AuthStore와 GNB 렌더링 이슈

  1. authStore 하위에서 children을 return, gnb만 initializing 이후에 렌더
	return !isInitializing ? (
		<ReactChannelIO
			pluginKey={CHANNEL_ID_PLUGIN_KEY}
			language="ko"
			autoBoot={CHANNEL_USE_AUTO_BOOT}
			{...(isLoggedIn &&
				user?.id && {
					memberId: `${user.id}`,
					profile: { name: user.displayName },
				})}>
			{children}
		</ReactChannelIO>
	) : (
		<>{children}</> //as-is null
	)
})
  • 문제 : 로그인 정보는 클라이언트에서 확인되어 gnb에서 상태 reflow가 발생
  1. GNB만 initializing된 이후에 렌더
	<WebViewWrapper>
			{!isInitializing && isUseFooter && renderGNB}
			<Content>{children}</Content>
			{isUseFooter && <FooterView />}
		</WebViewWrapper>
  • 문제 : gnb쪽에서 reflow가 발생.
  1. reflow 개선을 위해 gnb영역의 사이즈를 고정
  • 문제 : 리플로우는 개선되었지만 하위 컨텐츠와의 렌더링 시간차는 여전히 존재..
    • 그러면 컴포넌트 전체를 initializing ? : 페이지 자체가 콜드스타트 처럼 보임. + nextjs를 사용하는것이 의미가 있을까..?
  1. 서버사이드에서 유저정보를 받아보자.
MyApp.getInitialProps = async (appContext) => {  
	// calls page's `getInitialProps` and fills `appProps.pageProps`
	const appProps = await App.getInitialProps(appContext)
	const req = appContext.ctx.req
	let auth = req?.auth
	let { cookie } = appContext.ctx.req.headers
	let accessToken = cookie.split(';').find((c) => c.trim().startsWith('accessToken'))
	if (accessToken) {
		accessToken = accessToken.split('=')[1]
		try {
			let response = await getMe(accessToken)
			auth = { ...auth, ...response }
		} catch (e) {}
	}

	return { ...appProps, auth: auth }
}

해결..!

된줄 알았지만

  1. gnb가 mounted된 이후에 렌더되도록 변경
const mounted = useMounted()
	return (
		<WebViewWrapper>
			{mounted && isUseFooter && renderGNB}
  • 문제 : 리렌더링되는 부분때문에 약간의 깜박임 발생.

    • _app.tsx에서 2번 렌더링 되는것을 확인
    • 사용하지 않는 useState가 있어 제거 → 리렌더링 해결. 하지만 깜박임은 여전함.
    • layout 컴포넌트 내에서 useAuth() 훅 실행중 → serversideprops로 받아오기 때문에 제거 → 깜박임 해결
  1. reflow를 최소화하기 위해 height고정.

    	const renderGNB = useMemo(() => {
    		if (!mounted) return <SkeletonGNB/>
    		if (goodNotesPage) return <GoodnotesGNB />
    		if (expertPage) return <ExpertGNB />
    		return <MarketGNB />
    	}, [mounted, pathname])
    

csr 버전

  1. 결론 )
    • 초기 서버사이드 렌더링시 보여줄 수있는 <SkeletonGNB/> 를 생성하여 reflow를 최소화

이슈2. SEO 컴포넌트

  • expert component까지 ssr 적용확인.

해결..!

된줄 알았지만

  • 문제 : meta는 적용되지 않음.

  • SEO 공통컴포넌트가 아닌, next-seo 컴포넌트 사용시 meta 적용 확인

  • SEO 컴포넌트 내에서 props로 전송된 meta data가 useState와 useEffect로 변경되는로직으로 확인. 서버사이드에서 nextSeoOptions는 빈객체

const SEO = (props: SEOProps) => {
	console.log('SEO props',props)
	const [nextSeoOptions, setNextSeoOptions] = useState<NextSeoProps>({})

	useEffect(() => {
	//....생략
		setNextSeoOptions(transformedProps)
	}, [props])
	console.log('nextSeoOptions', nextSeoOptions)
  • useEffect에서 props를 변경해 주는 방식이 아닌, 바로 변수를 할당하는 식으로 변경 : 서버사이드 렌더링 시에는 매 요청마다 HTML을 생성하기 때문에 별도의 메모이제이션이나 최적화가 필요하지 않음. props가 변경되면 재렌더링이 되기 때문에 강제로 useState를 사용하지 않아도 될거같다고 판단.
const SEO = (props: SEOProps) => {
	const openGraph = _.pickBy(
		{
			url: props.ogUrl,
			title: props.ogTitle,
			description: props.ogDescription,
			images: _.identity(props.ogImage) ? [{ url: props.ogImage }] : undefined,
		},
		_.identity,
	)
	const transformedProps = _.pickBy(
		{
			title: props.title,
			description: props.description,

			additionalMetaTags: _.identity(props.keywords)
				? [
						{
							name: 'keywords',
							content: props.keywords,
						},
				  ]
				: undefined,
			openGraph: _.isEmpty(openGraph) ? undefined : openGraph,
		},
		_.identity,
	)
	return <NextSeo {...transformedProps} />

진짜 해결!


남은 개선 작업

  • 디자인 시스템의 useMediaQuery가 ssr에서도 작동하도록 개선
  • 검색 input의 placerholder관련 api도 serversideprops로 변경한다면 초기 렌더링 경험을 더욱 개선할 수 있지 않을까?

참고자료

https://wnsdufdl.tistory.com/524

https://davidhwang.netlify.app/TIL/(0320)nextjs에서-next-cookies-사용-이슈/