SEO를 고려할 때,
CSR 프로젝트에서는 단순히 헬멧(
react-helmet-async
)을 사용하는 것만으로는 SEO 문제를 완벽하게 해결할 수 없습니다. 주의해야 할 점은 대부분의 검색 엔진과 크롤러는 JavaScript를 작동하지 않고, 단순한 서버 응답을 기반으로 페이지를 색인합니다. 따라서 CSR의 경우 동적인 메타데이터가 아닌index.html
의 메타 데이터를 그대로 이용합니다. (헬멧으로 적용한 동적 메타데이터를 제외한 index.html의 메타 데이터만 조회 됨) 이러한 이유로 SEO 문제를 완전히 해결하려면 SSR을 이용하는 것이 더 적합합니다.
react-helmet
의 메모리 누수와 데이터 무결성 저하 등의 버그 개선 버전.<head>
태그를 설정 할 수 있음.npm i react-helmet-async
index.tsx(js)
파일에 import { HelmetProvider } from 'react-helmet-async';
를 추가하고,<App />
컴포넌트를 <HelmetProvider>
로 래핑.<Helmet>
컴포넌트는 항상 <HelmetProvider>
컴포넌트 안에 존재해야 하기 때문.<Helmet>
컴포넌트는 해당 페이지의 <head>
태그로 생각하면 됨.import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<BrowserRouter>
<HelmetProvider>
<App />
</HelmetProvider>
</BrowserRouter>
</React.StrictMode>
);
serviceWorkerRegistration.register();
reportWebVitals();
helmetContext
변수를 <HelmetProvider>
의 context
로 전달 해야함.import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
const helmetContext = {};
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<BrowserRouter>
<HelmetProvider context={helmetContext}>
<App />
</HelmetProvider>
</BrowserRouter>
</React.StrictMode>
);
serviceWorkerRegistration.register();
reportWebVitals();
import { Helmet } from 'react-helmet-async';
추가 뒤,<Helmet>
태그를 이용해 <head>
를 작성.react-helmet-async
는 뎁스(depth)가 큰(깊숙한 곳에 위치한) <Helmet>
이 우선권을 가짐.(오버라이드; Override)App.tsx(js)
에 <Helmet>
태그로 정의.index.html
에 존재하는 메타데이터와 중복이 없어야 함.App.tsx(js)
에서 디폴트 메타태그로 지정하고,robots
, API 권한 등)는 index.html
에서 지정하는 방법이 좋아보임.// App.tsx
import React, { useState, useEffect } from 'react';
import { Reset } from 'styled-reset';
import './style/App.scss';
import { Routes, Route } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';
import Header from './component/Header';
import Bus from './page/Bus';
import Footer from './component/Footer';
import MenuBox from './component/MenuBox';
import Home from './page/Home';
import SpinLogo from './component/SpinLogo';
import Meal from './page/Meal';
function App() {
return (
<>
<Reset />
<Helmet>
<title>더존 빵돌이</title>
<meta name='keywords' content='버스, 시간, 식당, 식단, 날씨, 회식장소, 빵돌이, 맛집, 카페, 생활정보' />
<meta name='description' content='구내식당 식단과 통근 버스의 실시간 도착 시간을 안내하는 더존 빵돌이 웹 서비스입니다.' />
{/* 오픈그래프(Open Graph) */}
<meta property='og:url' content='https://breadkun.com/' />
<meta property='og:title' content='더존 빵돌이' />
<meta property='og:type' content='website' />
<meta property='og:image' content='./logo/og-image.png' />
<meta property='og:description' content='구내식당 식단과 통근 버스의 실시간 도착 시간을 안내하는 더존 빵돌이 웹 서비스입니다.' />
</Helmet>
<Header setMenuBox={setMenuBox} />
<Routes>
<Route path='/' element={<Home setMenuBox={setMenuBox} />} />
<Route path='/meal' element={<Meal setMenuBox={setMenuBox} />} />
<Route path='/cafe' element={<SpinLogo text1={'COMMING SOON'} text2={'서비스 준비중입니다.'} minHeight='80vh' />} />
<Route path='/omakase' element={<SpinLogo text1={'COMMING SOON'} text2={'서비스 준비중입니다.'} minHeight='80vh' />} />
<Route path='/bus' element={<Bus setMenuBox={setMenuBox} />} />
<Route path='/bus/:destination' element={<Bus setMenuBox={setMenuBox} />} />
<Route path='*' element={<SpinLogo text1={'404 Not Found'} text2={'페이지를 찾을 수 없습니다.'} minHeight='80vh' />} />
</Routes>
<Footer />
</>
);
}
export default App;
// Meal.tsx
import React from 'react';
import { Helmet } from 'react-helmet-async';
function Meal({ setMenuBox }: { setMenuBox: React.Dispatch<React.SetStateAction<boolean>> }) {
return (
<>
<Helmet>
<title>더존 빵돌이 | 식단</title>
<meta name='keywords' content='식당, 식단, 날씨, 회식장소, 맛집, 카페' />
<meta name='description' content='더존ICT의 구내식당 식단입니다.' />
{/* 오픈그래프(Open Graph) */}
<meta property='og:url' content='https://breadkun.com/meal' />
<meta property='og:type' content='website' />
<meta property='og:image' content='./logo/og-image.png' />
<meta property='og:title' content='더존 빵돌이 | 식단' />
<meta property='og:description' content='더존ICT의 구내식당 식단입니다.' />
</Helmet>
<div className={ms('meal')}>
</div>
</>
);
}
export default Meal;
// BreadkunHelmet.tsx
import React from 'react';
import { Helmet } from 'react-helmet-async';
function BreadkunHelmet({ title, description, keywords, url, img }: { title: string; description: string; keywords: string; url: string; img: string }) {
return (
<Helmet>
<title>{title}</title>
<meta name='keywords' content={keywords} />
<meta name='description' content={description} />
{/* 오픈그래프(Open Graph) */}
<meta property='og:type' content='website' />
<meta property='og:url' content={url} />
<meta property='og:image' content={img} />
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
</Helmet>
);
}
BreadkunHelmet.defaultProps = {
title: '더존 빵돌이 | 다양한 더존ICT 생활 정보',
description: '더존ICT의 구내식당 식단, 통근 버스의 실시간 도착 시간, 오늘의 빵, 사내 카페 메뉴, 날씨 등 다양한 생활 정보를 안내하는 더존 빵돌이 웹 서비스입니다.',
keywords: '빵돌이, 더존ICT, 더존, 생활정보, 버스, 시간, 식당, 식단, 날씨, 회식장소, 맛집, 카페',
url: 'https://breadkun.com/',
img: './logo/og-image.png',
};
export default BreadkunHelmet;
글 잘 봤습니다.