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
+
+