요즘 Next.js 강의를 듣는 중인데 동적 meta tag를 설정하는 방법에 대한 내용이 있었습니다.
생각해보니 다른 웹 사이트를 이용할 때 쇼핑의 경우 상품 페이지를 공유하면 어떤 상품인지 드러나는 링크를 공유하게 되는데
현재 제 React 프로젝트의 경우 index.html 정적 페이지 하나에서만 meta tag를 설정하고 있기 때문에 상품링크를 공유해도 상품에 맞는 og 태그가 나타나지 않겠구나라고 깨닫게 되었습니다.
그래서 리액트에 동적 meta tag를 설정할 수 있는 react-helmet-async
를 사용하는 방법과 페이지를 pre-rendering할 수 있는 react-snap
에 대해 알아보고 직접 프로젝트에 적용해보았습니다!
react-helmet과 react-helmet-async의 차이점
react-helmet-async
This package is a fork of React Helmet. usage is synonymous, but server and client now requires to encapsulate state per request.react-helmet relies on react-side-effect, which is not thread-safe. If you are doing anything asynchronous on the server, you need Helmet to encapsulate data on a per-request basis, this package does just that.
react-helmet-async는 react-helmet를 기반으로 만들어진 라이브러리로 react-helmet의 주요 기능을 모두 포함하면서 동시에 비동기 방식으로 작동합니다.
또한, react-helmet과 다르게 react-side-effect에 의존하지 않고 안전한 비동기 처리를 지원합니다.
서버에서 비동기식 작업을 수행하는 경우 요청별로 데이터를 캡슐화하는 것을 react-helmet-async에서 지원한다는 의미입니다.
npm i react-helmet-async
<App />
을 HelmetProvider
로 감싸줍니다.index.tsx
import { HelmetProvider } from "react-helmet-async";
const container = document.getElementById("root")!;
const root = createRoot(container);
root.render(
<BrowserRouter>
<RecoilRoot>
<ScrollToTop />
<HelmetProvider>
<App />
</HelmetProvider>
</RecoilRoot>
</BrowserRouter>
);
<Helmet>
안에서 title, meta tag 등을 작성해주면됩니다.// example
<Helmet>
<title>My Title</title>
<meta name="description" content="Helmet application" />
</Helmet>
저의 경우 쇼핑몰 페이지이기 때문에 상품페이지에 대한 설명을 props로 전달받기 위해 MetaTag.tsx를 따로 생성하여 필요한 페이지에서 불러와 사용하는 방식을 선택했습니다.
MetaTag.tsx
import { Helmet } from "react-helmet-async";
interface MetaTagProps {
title?: string;
description?: string;
imgSrc?: string;
url?: string;
}
const DEPLOY_URL = "배포 URL";
export default function MetaTag(props: MetaTagProps) {
const url = props.url ? `${DEPLOY_URL}${props.url}` : DEPLOY_URL;
return (
<Helmet>
<title>{(props.title && `default title - ${props.title}`) || "default title"}</title>
<meta name="description" content={props.description || "default description"}/>
<meta property="og:type" content="website" />
<meta property="og:title" content={props.title || "default title"} />
<meta property="og:site_name" content="default title" />
<meta property="og:description" content={props.description || "default description"} />
<meta property="og:image" content={props.imgSrc || "default image"} />
<meta property="og:url" content={url} />
</Helmet>
);
}
Default로 적용하기위해서 App에서 MetaTag를 사용하였습니다.
App.tsx
import MetaTag from "./components/common/MetaTag";
function App() {
return (
<Wrap>
<MetaTag />
<GlobalStyle />
<Layout>
<Routers />
<ModalContainer>
<Modals />
</ModalContainer>
</Layout>
<ToastContainer transition={Zoom} />
</Wrap>
);
}
export default App;
ProductDetail에서 적용한 모습
ProductDetail.tsx
export default function ProductDetail(){
...
return (
<MetaTag title={product_name} description={product_info} imgSrc={image} url={path} />
...
)
}
잘 적용되고 있는 모습을 확인할 수 있습니다.
하지만 여전히 크롤러는 index.html 하나만 탐색하기 때문에 SNS 공유를 하게 되면 바뀐 og 태그가 제대로 적용되지 않고 있습니다.
이를 해결하기 위해 react-snap
을 사용할 수 있습니다.
npm i react-snap
index.tsx를 다음과 같이 변경해주면됩니다.
index.tsx
import ReactDOM from "react-dom/client";
//..생략..
const container = document.getElementById("root")!;
const root = createRoot(container);
if (container.hasChildNodes()) {
ReactDOM.hydrateRoot(
container,
<BrowserRouter>
<RecoilRoot>
<ScrollToTop />
<HelmetProvider>
<App />
</HelmetProvider>
</RecoilRoot>
</BrowserRouter>
);
} else {
root.render(
<BrowserRouter>
<RecoilRoot>
<ScrollToTop />
<HelmetProvider>
<App />
</HelmetProvider>
</RecoilRoot>
</BrowserRouter>
);
}
package.json
"script": {
...
"postbuild": "react-snap" // 추가
},
"reactSnap": {
"include": [
// 포함할 경로 작성하기
"/"
],
"exclude": [
// 제외할 경로 작성하기
],
}
그러고 나서 build를 진행해주면 각 path별로 index.html이 생성되게 됩니다.
❗️여기서 문제가 있는데 상품의 상세페이지의 경우 /detail/:productId
로 동적 라우팅이 설정되어있는데, 맨 처음 렌더링되는 30개의 상품만 index.html이 만들어졌다는 것입니다.
react-snap을 통해 index.html이 만들어진 상품은 아래처럼 상품의 이미지, 제목, 설명이 잘 나타나지만 그렇지 않은 경우 default로 설정된 og 태그가 나타나게 됩니다.
동적 라우팅과 관련해서 해당 Issue를 살펴보았으나 react-snap의 경우 동적 라우팅을 처리하는 것은 어려운 것 같습니다.
결론은.. 쇼핑몰이나 SEO가 중요한 사이트의 경우 SSR을 진행하는 것이 맞을 것 같습니다 🤔
참고
react-snap
https://velog.io/@euisuk95/React-Helmet%EA%B3%BC-React-Snap%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-SEO
https://velog.io/@chl4842/react-helmet-react-snap-%EC%9C%BC%EB%A1%9C-%EB%A9%94%ED%83%80%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B5%9C%EC%A0%81%ED%99%94%ED%95%98%EA%B8%B0#react-snap
https://velog.io/@apro_xo/react-helmet-async-react-snap
https://velog.io/@miyoni/noSSRyesSEO