From 731be17529947a88ed92bff99ed46ec63c2220f4 Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:26:40 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20react-helmet-async=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20SEO=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - react-helmet-async 패키지 설치 (v3.0.0) - main.tsx에 HelmetProvider 추가 - 공통 SEOHead 컴포넌트 생성 (src/components/common/SEOHead.tsx) - title, description props 기반 동적 메타태그 주입 - 기본값: 기존 index.html의 title/description 유지 --- package.json | 1 + pnpm-lock.yaml | 40 +++++++++++++++++++++++++++++++ src/components/common/SEOHead.tsx | 22 +++++++++++++++++ src/main.tsx | 7 +++++- 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/components/common/SEOHead.tsx diff --git a/package.json b/package.json index 5240acc..c239f30 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "react-datepicker": "^8.4.0", "react-dom": "^19.1.0", "react-ga4": "^2.1.0", + "react-helmet-async": "^3.0.0", "react-router-dom": "^7.6.0", "zustand": "^5.0.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d47d14..324f7d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: react-ga4: specifier: ^2.1.0 version: 2.1.0 + react-helmet-async: + specifier: ^3.0.0 + version: 3.0.0(react@19.1.0) react-router-dom: specifier: ^7.6.0 version: 7.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -1151,6 +1154,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1226,6 +1232,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1418,9 +1428,17 @@ packages: peerDependencies: react: ^19.1.0 + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + react-ga4@2.1.0: resolution: {integrity: sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==} + react-helmet-async@3.0.0: + resolution: {integrity: sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -1506,6 +1524,9 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2832,6 +2853,10 @@ snapshots: inherits@2.0.4: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ipaddr.js@1.9.1: {} is-arrayish@0.2.1: {} @@ -2887,6 +2912,10 @@ snapshots: lodash.merge@4.6.2: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -3050,8 +3079,17 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-fast-compare@3.2.2: {} + react-ga4@2.1.0: {} + react-helmet-async@3.0.0(react@19.1.0): + dependencies: + invariant: 2.2.4 + react: 19.1.0 + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + react-is@16.13.1: {} react-refresh@0.17.0: {} @@ -3161,6 +3199,8 @@ snapshots: setprototypeof@1.2.0: {} + shallowequal@1.1.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/src/components/common/SEOHead.tsx b/src/components/common/SEOHead.tsx new file mode 100644 index 0000000..76e7d08 --- /dev/null +++ b/src/components/common/SEOHead.tsx @@ -0,0 +1,22 @@ +import { Helmet } from 'react-helmet-async'; + +interface SEOHeadProps { + title?: string; + description?: string; +} + +const DEFAULT_TITLE = 'THIP, 독서를 기록하는 가장 힙한 방법'; +const DEFAULT_DESC = '커뮤니티형 독서 기록 플랫폼 THIP'; + +const SEOHead = ({ title, description }: SEOHeadProps) => { + const fullTitle = title ? `${title} - THIP` : DEFAULT_TITLE; + + return ( + + {fullTitle} + + + ); +}; + +export default SEOHead; diff --git a/src/main.tsx b/src/main.tsx index 8cda91d..62e7edb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,8 +1,13 @@ import { createRoot } from 'react-dom/client'; +import { HelmetProvider } from 'react-helmet-async'; import './main.css'; import App from './App.tsx'; import { initGA } from './shared/lib/analytics/ga'; initGA(); -createRoot(document.getElementById('root')!).render(); \ No newline at end of file +createRoot(document.getElementById('root')!).render( + + + , +); From 8570567da6de9bcb3e6fa1d03c7c5ee5e459a399 Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:27:13 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=B1=85=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8F=85=EC=84=9C=EB=AA=A8=EC=9E=84=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20SEO=20=EB=A9=94?= =?UTF-8?q?=ED=83=80=ED=83=9C=EA=B7=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SearchBook.tsx: 책 제목(title), 저자명(authorName) 기반 동적 title/description 주입 - GroupDetail.tsx: 모임명(roomName), 책 제목(bookTitle) 기반 동적 title/description 주입 - 데이터 로드 전(null 상태)에는 기본값 유지 --- src/pages/groupDetail/GroupDetail.tsx | 9 ++++++--- src/pages/searchBook/SearchBook.tsx | 7 +++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pages/groupDetail/GroupDetail.tsx b/src/pages/groupDetail/GroupDetail.tsx index 9bcf859..2408228 100644 --- a/src/pages/groupDetail/GroupDetail.tsx +++ b/src/pages/groupDetail/GroupDetail.tsx @@ -47,6 +47,7 @@ import bookCoverLargeImg from '../../assets/books/bookCoverLarge.svg'; import PasswordModal from '@/components/group/PasswordModal'; import { usePopupStore } from '@/stores/popupStore'; import { BannerSkeleton, BookSkeleton } from '@/shared/ui/Skeleton'; +import SEOHead from '@/components/common/SEOHead'; const GroupDetail = () => { const { roomId } = useParams<{ roomId: string }>(); @@ -91,9 +92,7 @@ const GroupDetail = () => { setError(null); const minLoadingTime = new Promise(resolve => setTimeout(resolve, 500)); - const [response] = await Promise.all([ - getRoomDetail(Number(roomId)), - ]); + const [response] = await Promise.all([getRoomDetail(Number(roomId))]); await minLoadingTime; if (response.isSuccess) { @@ -293,6 +292,10 @@ const GroupDetail = () => { return ( +
diff --git a/src/pages/searchBook/SearchBook.tsx b/src/pages/searchBook/SearchBook.tsx index 5a4eb4c..4318941 100644 --- a/src/pages/searchBook/SearchBook.tsx +++ b/src/pages/searchBook/SearchBook.tsx @@ -42,6 +42,7 @@ import { usePopupStore } from '@/stores/popupStore'; import { FeedPostSkeleton, BookDetailSkeleton } from '@/shared/ui/Skeleton'; import { usePreventDoubleClick } from '@/hooks/usePreventDoubleClick'; import { useInifinieScroll } from '@/hooks/useInifinieScroll'; +import SEOHead from '@/components/common/SEOHead'; const FILTER = ['최신순', '인기순'] as const; const toFeedSort = (f: (typeof FILTER)[number]): FeedSort => (f === '최신순' ? 'latest' : 'like'); @@ -212,6 +213,12 @@ const SearchBook = () => { return ( + {bookDetail && ( + + )} {bookDetail && }
From 5dc7646772faf4fdd14bc28d8418c9dfd653b21a Mon Sep 17 00:00:00 2001 From: Ji Ho June <129824629+ho0010@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:31:26 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20SEO=20=ED=81=AC=EB=A1=A4=EB=A7=81?= =?UTF-8?q?=20=EC=9D=B8=ED=94=84=EB=9D=BC=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - public/robots.txt 추가: 크롤러 허용/차단 경로 명시 - public/sitemap.xml 추가: 주요 경로 정적 sitemap 구성 - Google Search Console 제출로 인덱싱 속도 단축 예정 --- public/robots.txt | 8 ++++++++ public/sitemap.xml | 15 +++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..50c0a6a --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,8 @@ +User-agent: * +Allow: /search/book/ +Allow: /group/detail/ +Allow: /feed/ +Disallow: /mypage +Disallow: /signup +Disallow: /memory/ +Sitemap: https://thip.co.kr/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..9b9e392 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,15 @@ + + + + https://thip.co.kr/ + 1.0 + + + https://thip.co.kr/feed + 0.8 + + + https://thip.co.kr/group + 0.8 + +