Feat/#120 홈페이지(Home) SSG 카테고리 & CSR 랭킹 하이브리드 렌더링 구현#121
Feat/#120 홈페이지(Home) SSG 카테고리 & CSR 랭킹 하이브리드 렌더링 구현#121leeleeleeleejun merged 6 commits intodevelopfrom
Conversation
- rewrite 때문에 빌드 시 에러 발생 - NEXT_PHASE 환경 변수를 globalEnv에 추가
- 불필요한 HydrationBoundaryPage 제거 및 구조 단순화
- RankingSection을 서버 컴포넌트로 전환하여 UI 구조 담당 - RankingListFetcher 생성: 클라이언트에서 실시간 랭킹 데이터 조회 - RankingListFetcherClient 생성: next/dynamic으로 CSR 보장 (ssr: false) - 홈 페이지 SSG를 유지하면서 랭킹 데이터는 클라이언트에서만 렌더링
Walkthrough이 PR은 RankingListFetcher 및 RankingListFetcherClient 컴포넌트를 도입하고, Categories를 서버 컴포넌트로 전환하며, 홈 페이지를 강제 정적 렌더링으로 업데이트합니다. 또한 axiosInstance가 빌드 타임과 런타임 URL 선택을 지원하도록 수정되었습니다. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.Change the |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
apps/web/app/(home)/_components/RankingSection/RankingListFetcher.tsx (1)
3-8: import 순서가 저장소 규칙과 다릅니다.Line 3-8에서 외부 패키지(
@tanstack/react-query)가 absolute import 사이에 섞여 있습니다.🔧 제안 패치
import type { RankingPlaceSort } from '@/_apis/schemas/place' import { useCampusStore } from '@/_store/campus' import { useSuspenseQuery } from '@tanstack/react-query' import { usePlaceQueries } from '@/_apis/queries/place' import { EmptyFallback } from '@/_components/EmptyFallback' import { PlaceListItem } from '@/_components/PlaceListItem' +import { useSuspenseQuery } from '@tanstack/react-query'As per coding guidelines "Organize imports in the following order: Node/built-in, external packages, absolute imports, relative imports".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(home)/_components/RankingSection/RankingListFetcher.tsx around lines 3 - 8, The imports in RankingListFetcher.tsx are out of the project's ordering rules: the external package import useSuspenseQuery from '@tanstack/react-query' is currently between absolute imports. Move the external import so that all external/package imports (e.g., useSuspenseQuery) appear after built-ins (if any) and before absolute imports (any paths starting with '@/...'), keeping the other symbols (RankingPlaceSort, useCampusStore, usePlaceQueries, EmptyFallback, PlaceListItem) in the absolute imports group; then save/format to preserve grouping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/app/_components/Categories/Categories.tsx`:
- Around line 5-7: The Categories server component currently awaits
getCategories() and lets exceptions bubble to SSG; wrap the getCategories() call
in a try/catch inside the async Categories function, call console.error with the
caught error (including context like "Categories fetch failed"), and fall back
to a safe minimal value (e.g., const categories: Category[] = [] or a single
"All" category) so the component can render during build-time failures; also
propagate this error handling into any getCategories onError callback if one
exists so it logs and returns the same fallback instead of throwing.
In `@apps/web/app/_lib/axiosInstance.ts`:
- Around line 11-30: The getBaseURL function lacks an explicit return type and
the axios request interceptor reads cookies on the server causing dynamic cookie
API calls; update getBaseURL to declare a return type string (change to const
getBaseURL = (): string => {...}) and modify the axiosInstance request
interceptor (the code that calls getCookie('accessToken')) to only read cookies
in the browser (guard with typeof window !== 'undefined' or similar) so
server-side/static builds never invoke cookies()/getCookie; ensure axiosInstance
still sets baseURL via getBaseURL() after the type change.
- Around line 12-27: The getBaseURL function lacks an explicit return type; add
a return type annotation (e.g., getBaseURL(): string | undefined) to satisfy
strict TypeScript and noUncheckedIndexedAccess, and ensure all return paths (the
build-time branch returning process.env.NEXT_PUBLIC_API_URL_BUILD ||
process.env.NEXT_PUBLIC_API_URL and the default returning
process.env.NEXT_PUBLIC_API_URL) conform to that type; update the function
signature in apps/web/app/_lib/axiosInstance.ts to include the explicit return
type.
In `@apps/web/app/`(home)/_components/RankingSection/RankingListFetcher.tsx:
- Around line 10-14: The component RankingListFetcher is missing an explicit
return type; update its function signature to declare a React return type (e.g.
change "export const RankingListFetcher = ({ rankingPlaceSort }: {
rankingPlaceSort: RankingPlaceSort }) => {" to include ": React.ReactElement" or
": React.ReactElement | null" as appropriate) so the component has a concrete
TypeScript return type; ensure React is imported for the type if needed (import
type { ReactElement } from 'react' and use ": ReactElement" or use
"JSX.Element").
In `@turbo.json`:
- Line 4: The globalEnv array in turbo.json is missing API URL related keys
(NEXT_PUBLIC_API_URL, NEXT_PUBLIC_API_URL_BUILD, NEXT_PUBLIC_API_URL_HOST,
NEXT_PUBLIC_API_URL_BASE), so add those four entries to the "globalEnv" list to
ensure changes to API endpoints invalidate Turbo cache; update the "globalEnv"
value (the symbol to change is globalEnv in turbo.json) to include these keys
alongside NODE_ENV, NEXT_RUNTIME, CI, NEXT_PHASE.
---
Nitpick comments:
In `@apps/web/app/`(home)/_components/RankingSection/RankingListFetcher.tsx:
- Around line 3-8: The imports in RankingListFetcher.tsx are out of the
project's ordering rules: the external package import useSuspenseQuery from
'@tanstack/react-query' is currently between absolute imports. Move the external
import so that all external/package imports (e.g., useSuspenseQuery) appear
after built-ins (if any) and before absolute imports (any paths starting with
'@/...'), keeping the other symbols (RankingPlaceSort, useCampusStore,
usePlaceQueries, EmptyFallback, PlaceListItem) in the absolute imports group;
then save/format to preserve grouping.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c567f069-d291-49cf-bcb9-8939303af312
📒 Files selected for processing (7)
apps/web/app/(home)/_components/RankingSection/RankingListFetcher.tsxapps/web/app/(home)/_components/RankingSection/RankingListFetcherClient.tsxapps/web/app/(home)/_components/RankingSection/RankingSection.tsxapps/web/app/(home)/page.tsxapps/web/app/_components/Categories/Categories.tsxapps/web/app/_lib/axiosInstance.tsturbo.json
| export const Categories = async () => { | ||
| const categories = await getCategories() | ||
|
|
There was a problem hiding this comment.
카테고리 API 실패가 SSG 전체 실패로 전파됩니다.
Line 6에서 예외를 그대로 전파하면 빌드 타이밍의 일시적 API 장애가 홈 페이지 정적 생성 실패로 이어질 수 있습니다. 최소한의 fallback과 로깅을 넣어 주세요.
🔧 제안 패치
-export const Categories = async () => {
- const categories = await getCategories()
+export const Categories = async (): Promise<JSX.Element> => {
+ let categories = []
+ try {
+ categories = await getCategories()
+ } catch (error) {
+ console.error('Failed to fetch categories in Categories', error)
+ }
return (As per coding guidelines "Handle errors with console.error and implement error handling logic in onError callbacks".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/app/_components/Categories/Categories.tsx` around lines 5 - 7, The
Categories server component currently awaits getCategories() and lets exceptions
bubble to SSG; wrap the getCategories() call in a try/catch inside the async
Categories function, call console.error with the caught error (including context
like "Categories fetch failed"), and fall back to a safe minimal value (e.g.,
const categories: Category[] = [] or a single "All" category) so the component
can render during build-time failures; also propagate this error handling into
any getCategories onError callback if one exists so it logs and returns the same
fallback instead of throwing.
| export const RankingListFetcher = ({ | ||
| rankingPlaceSort, | ||
| }: { | ||
| rankingPlaceSort: RankingPlaceSort | ||
| }) => { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f "RankingListFetcher.tsx" --exec cat -n {}Repository: knu-matzip/Matzip-Client
Length of output: 1318
컴포넌트의 명시적 반환 타입을 추가하세요.
RankingListFetcher는 TypeScript 엄격 모드에서 필수적으로 반환 타입을 선언해야 합니다. 현재 함수 시그니처에 반환 타입이 누락되어 있습니다.
🔧 제안 패치
export const RankingListFetcher = ({
rankingPlaceSort,
}: {
rankingPlaceSort: RankingPlaceSort
-}) => {
+}): JSX.Element => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/app/`(home)/_components/RankingSection/RankingListFetcher.tsx around
lines 10 - 14, The component RankingListFetcher is missing an explicit return
type; update its function signature to declare a React return type (e.g. change
"export const RankingListFetcher = ({ rankingPlaceSort }: { rankingPlaceSort:
RankingPlaceSort }) => {" to include ": React.ReactElement" or ":
React.ReactElement | null" as appropriate) so the component has a concrete
TypeScript return type; ensure React is imported for the type if needed (import
type { ReactElement } from 'react' and use ": ReactElement" or use
"JSX.Element").
| "$schema": "https://turborepo.com/schema.json", | ||
| "ui": "tui", | ||
| "globalEnv": ["NODE_ENV","NEXT_PUBLIC_API_URL", "NEXT_RUNTIME", "CI"], | ||
| "globalEnv": ["NODE_ENV", "NEXT_RUNTIME", "CI", "NEXT_PHASE"], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python - <<'PY'
import json, pathlib, re
turbo = json.loads(pathlib.Path("turbo.json").read_text())
global_env = set(turbo.get("globalEnv", []))
targets = [
"apps/web/app/_lib/axiosInstance.ts",
"apps/web/middleware.ts",
"apps/web/next.config.ts",
]
used = set()
for f in targets:
p = pathlib.Path(f)
if not p.exists():
continue
used |= set(re.findall(r'process\.env\.([A-Z0-9_]+)', p.read_text()))
focus = sorted([k for k in used if k.startswith("NEXT_PUBLIC_API_URL") or k == "NEXT_PHASE"])
missing = sorted(set(focus) - global_env)
print("globalEnv:", sorted(global_env))
print("used env keys:", focus)
print("missing from globalEnv:", missing)
PYRepository: knu-matzip/Matzip-Client
Length of output: 398
globalEnv에서 API URL 계열 키가 빠져 Turbo 캐시가 오염될 수 있습니다.
Line 4의 globalEnv 설정에서 NEXT_PUBLIC_API_URL, NEXT_PUBLIC_API_URL_BUILD, NEXT_PUBLIC_API_URL_HOST, NEXT_PUBLIC_API_URL_BASE가 누락되어 있습니다. 이 환경 변수들이 변경되어도 빌드 캐시가 무효화되지 않아, 오래된 API 엔드포인트 설정이 재사용될 위험이 있습니다.
🔧 제안 패치
- "globalEnv": ["NODE_ENV", "NEXT_RUNTIME", "CI", "NEXT_PHASE"],
+ "globalEnv": [
+ "NODE_ENV",
+ "NEXT_RUNTIME",
+ "CI",
+ "NEXT_PHASE",
+ "NEXT_PUBLIC_API_URL",
+ "NEXT_PUBLIC_API_URL_BUILD",
+ "NEXT_PUBLIC_API_URL_HOST",
+ "NEXT_PUBLIC_API_URL_BASE"
+ ],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "globalEnv": ["NODE_ENV", "NEXT_RUNTIME", "CI", "NEXT_PHASE"], | |
| "globalEnv": [ | |
| "NODE_ENV", | |
| "NEXT_RUNTIME", | |
| "CI", | |
| "NEXT_PHASE", | |
| "NEXT_PUBLIC_API_URL", | |
| "NEXT_PUBLIC_API_URL_BUILD", | |
| "NEXT_PUBLIC_API_URL_HOST", | |
| "NEXT_PUBLIC_API_URL_BASE" | |
| ], |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@turbo.json` at line 4, The globalEnv array in turbo.json is missing API URL
related keys (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_API_URL_BUILD,
NEXT_PUBLIC_API_URL_HOST, NEXT_PUBLIC_API_URL_BASE), so add those four entries
to the "globalEnv" list to ensure changes to API endpoints invalidate Turbo
cache; update the "globalEnv" value (the symbol to change is globalEnv in
turbo.json) to include these keys alongside NODE_ENV, NEXT_RUNTIME, CI,
NEXT_PHASE.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/web/app/_lib/axiosInstance.ts (1)
72-96: 응답 인터셉터에 빌드 시점 가드 추가 권장응답 인터셉터의 401 토큰 갱신 로직(라인 72-96)에
isBuildTime가드가 없어 빌드 시점에 401 응답 발생 시getToken()과setCookie()가 서버 컨텍스트에서 실행될 수 있습니다. 현재 카테고리 API는 공개 엔드포인트이지만, 향후 빌드 시점에 인증이 필요한 데이터를 페칭하는 경우에 대비해 다음 패턴을 고려하세요:if (error.response?.status === 401 && !originalRequest._retry) { if (isBuildTime) return Promise.reject(error) // ... 기존 토큰 갱신 로직 }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/_lib/axiosInstance.ts` around lines 72 - 96, Add a build-time guard inside the response interceptor's 401 retry block: before calling getToken() or setCookie() (inside the if (error.response?.status === 401 && !originalRequest._retry) branch in the axios response interceptor), check if isBuildTime and immediately return Promise.reject(error) to avoid executing getToken, setCookie, or retrying the request during server/build execution; preserve setting originalRequest._retry and the existing refresh flow (getToken, setCookie, update originalRequest.headers.Authorization, and return axiosInstance(originalRequest)) for non-build-time runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/app/_lib/axiosInstance.ts`:
- Around line 20-27: The getBaseURL function uses non-null assertions on
process.env.NEXT_PUBLIC_API_URL and NEXT_PUBLIC_API_URL_BUILD which can cause
runtime errors if those env vars are missing; update getBaseURL to safely read
those variables (no "!"), validate their presence at runtime and either throw a
clear error or return a sensible fallback, e.g., check isBuildTime and prefer
process.env.NEXT_PUBLIC_API_URL_BUILD ?? process.env.NEXT_PUBLIC_API_URL, and if
still undefined throw a descriptive Error mentioning
NEXT_PUBLIC_API_URL/NEXT_PUBLIC_API_URL_BUILD; also ensure your CI/build config
(e.g., turbo.json globalEnv) includes these keys so they are available at build
time.
---
Nitpick comments:
In `@apps/web/app/_lib/axiosInstance.ts`:
- Around line 72-96: Add a build-time guard inside the response interceptor's
401 retry block: before calling getToken() or setCookie() (inside the if
(error.response?.status === 401 && !originalRequest._retry) branch in the axios
response interceptor), check if isBuildTime and immediately return
Promise.reject(error) to avoid executing getToken, setCookie, or retrying the
request during server/build execution; preserve setting originalRequest._retry
and the existing refresh flow (getToken, setCookie, update
originalRequest.headers.Authorization, and return
axiosInstance(originalRequest)) for non-build-time runs.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 67fdfeeb-8705-4ab0-b22d-1fa4533e95e5
📒 Files selected for processing (2)
apps/web/app/_lib/axiosInstance.tsapps/web/app/profile/page.tsx
| const getBaseURL = (): string => { | ||
| if (isBuildTime) { | ||
| return ( | ||
| process.env.NEXT_PUBLIC_API_URL_BUILD || process.env.NEXT_PUBLIC_API_URL! | ||
| ) | ||
| } | ||
| return process.env.NEXT_PUBLIC_API_URL! | ||
| } |
There was a problem hiding this comment.
환경 변수에 non-null assertion(!) 사용은 런타임 오류 위험이 있습니다.
process.env.NEXT_PUBLIC_API_URL!에 non-null assertion을 사용하면 TypeScript 타입 검사를 우회하게 됩니다. strict 모드와 noUncheckedIndexedAccess가 활성화된 상태에서, 환경 변수가 설정되지 않은 경우 undefined가 baseURL로 전달되어 런타임 오류가 발생할 수 있습니다.
또한 turbo.json의 globalEnv에 NEXT_PUBLIC_API_URL과 NEXT_PUBLIC_API_URL_BUILD가 포함되어 있지 않아 빌드 시점에 환경 변수가 누락될 위험이 있습니다.
🔧 제안 패치
const getBaseURL = (): string => {
+ const buildUrl = process.env.NEXT_PUBLIC_API_URL_BUILD
+ const defaultUrl = process.env.NEXT_PUBLIC_API_URL
+
if (isBuildTime) {
- return (
- process.env.NEXT_PUBLIC_API_URL_BUILD || process.env.NEXT_PUBLIC_API_URL!
- )
+ const url = buildUrl ?? defaultUrl
+ if (!url) {
+ throw new Error('API URL이 설정되지 않았습니다: NEXT_PUBLIC_API_URL_BUILD 또는 NEXT_PUBLIC_API_URL 환경 변수를 확인하세요.')
+ }
+ return url
+ }
+
+ if (!defaultUrl) {
+ throw new Error('API URL이 설정되지 않았습니다: NEXT_PUBLIC_API_URL 환경 변수를 확인하세요.')
}
- return process.env.NEXT_PUBLIC_API_URL!
+ return defaultUrl
}Based on learnings: "Use TypeScript in strict mode with noUncheckedIndexedAccess enabled and return types required on functions"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/app/_lib/axiosInstance.ts` around lines 20 - 27, The getBaseURL
function uses non-null assertions on process.env.NEXT_PUBLIC_API_URL and
NEXT_PUBLIC_API_URL_BUILD which can cause runtime errors if those env vars are
missing; update getBaseURL to safely read those variables (no "!"), validate
their presence at runtime and either throw a clear error or return a sensible
fallback, e.g., check isBuildTime and prefer
process.env.NEXT_PUBLIC_API_URL_BUILD ?? process.env.NEXT_PUBLIC_API_URL, and if
still undefined throw a descriptive Error mentioning
NEXT_PUBLIC_API_URL/NEXT_PUBLIC_API_URL_BUILD; also ensure your CI/build config
(e.g., turbo.json globalEnv) includes these keys so they are available at build
time.
#️⃣연관된 이슈
📝작업 내용
홈 페이지를 SSG로 최적화하면서 사용자별 실시간 데이터(랭킹)는 CSR로 처리하는 하이브리드 렌더링 전략을 구현했습니다
1. 홈 페이지 SSG 설정
2. Categories 섹션 SSG 구현
3. Ranking 섹션 CSR 구현
스크린샷 (선택)
빌드 결과물(index.html)에 카테고리 데이터가 포함된 모습 (랭킹 데이터는 미포함)

실제 동작
💬리뷰 요구사항(선택)
Summary by CodeRabbit
릴리스 노트
New Features
Refactor
Chores