딜러 관리자 웹 작업을 하면서 리뷰 때 반복적으로 나온 질문들을 정리했다. 리뷰는 결국 “왜 이렇게 했어?”의 연속인데, 좋은 질문일수록 그 자리에서 답하기가 애매하다. 나중에 또 나올 질문이라 아예 답을 문서로 박아둔다. 실제로 딜러 관리자 웹에서 걸렸던 주제 위주로 적었다.

딜러 관리자 웹은 Next.js 기반이다. 아래 질문들은 이 저장소의 실제 작업에서 나온 것들이다.

Q1. 약관 페이지, 왜 노션 API로 읽어오나? 그냥 하드코딩하면 안 돼?

이용약관 같은 정적 문구는 처음엔 컴포넌트에 하드코딩돼 있었다. 문제는 비개발자가 문구를 못 고친다는 것. 약관 한 줄 바꾸려고 배포를 태우는 게 낭비였다.

그래서 노션을 CMS처럼 썼다. 노션 공식 API로 페이지를 읽어와 앱 자체 페이지로 렌더링한다. 문구는 노션에서 관리하니 비개발자가 직접 수정하고, 재배포가 필요 없다.

// src/api/fetchTerms.ts (요지)
import { Client } from '@notionhq/client';
import { NotionToMarkdown } from 'notion-to-md';
import { marked } from 'marked';

// ⚠️ NOTION_TOKEN 은 서버 전용 시크릿. getStaticProps 안에서만 import 한다.
const notion = new Client({ auth: process.env.NOTION_TOKEN });
const n2m = new NotionToMarkdown({ notionClient: notion });

marked.setOptions({ gfm: true, breaks: true }); // 노션 소프트 개행도 <br>로 살림

export async function fetchTermsHtml(pageId: string): Promise<string> {
  const mdBlocks = await n2m.pageToMarkdown(pageId);
  const markdown = n2m.toMarkdownString(mdBlocks).parent ?? '';
  return marked.parse(markdown); // 노션 → 마크다운 → HTML
}

Q2. 그럼 매 요청마다 노션을 때리나? 느리지 않나?

아니다. 이게 리뷰에서 꼭 나오는 질문이다. 매 요청 노션을 호출하면 느리고, 노션 장애가 곧 우리 페이지 장애가 된다.

그래서 ISR(Incremental Static Regeneration) 을 썼다. getStaticProps에서 노션을 읽어 정적으로 만들어두고, revalidate로 주기적으로만 갱신한다.

export const getStaticProps = async () => {
  const html = await fetchTermsHtml(TERMS_PAGE_ID);
  return {
    props: { html },
    revalidate: 3600, // 1시간마다 백그라운드 재생성
  };
};
  • 사용자는 항상 캐시된 정적 페이지를 받는다 → 빠르다.
  • 노션에서 약관을 고치면 최대 1시간 뒤 자동 반영 → 재배포 불필요.
  • 노션이 잠깐 죽어도 마지막으로 생성된 정적 페이지가 떠 있다 → 장애 격리.

“실시간 반영이 필요한가?”를 먼저 물어야 한다. 약관은 1시간 지연이 전혀 문제가 안 되는 데이터라 ISR이 정확히 맞았다.

Q3. NOTION_TOKEN 이 클라이언트로 새어나가지 않나?

리뷰에서 반드시 확인하는 지점이다. 서버 전용 시크릿이 클라이언트 번들에 들어가면 사고다.

  • fetchTerms.tsgetStaticProps 안에서만 import 한다. Next.js는 getStaticProps/서버 코드에서만 쓰인 모듈을 클라이언트 번들에서 제외한다.
  • 환경변수 이름에 NEXT_PUBLIC_ 접두사를 붙이지 않는다. 이 접두사가 있으면 클라이언트로 노출된다.
  • 파일 상단에 “이 파일은 서버에서만 import 하라”는 주석을 남겨서, 다음 사람이 실수로 컴포넌트에서 import 하지 않게 한다.

💡 시크릿 관련 리뷰 질문의 핵심은 “이 토큰이 브라우저까지 가느냐”다. import 경로와 접두사 규칙 두 개만 지켜도 대부분 막힌다.

Q4. 배포 워크플로우에서 GITHUB_TOKEN 권한은 왜 명시했나?

GitHub Actions의 GITHUB_TOKEN은 아무것도 안 적으면 넓은 기본 권한을 갖는다. 리뷰에서 “이 워크플로우가 대체 무슨 권한을 쓰는데?”라는 질문이 나왔고, 답이 애매하면 그 자체가 문제다.

그래서 워크플로우 레벨에서 최소 권한 원칙으로 명시했다.

# 워크플로우가 어떤 권한을 쓰는지 명확히, 탈취돼도 최소 권한만 노출
permissions:
  contents: read
  packages: read
  • 워크플로우가 필요한 권한만 선언 → 읽는 사람이 의도를 안다.
  • 토큰이 탈취돼도 노출되는 권한이 read로 제한된다.

리뷰 관점에서 좋은 습관이다. “기본값이 뭔지 모르는 권한”을 코드에 남기지 않는 것.

Q5. 팝업/배너 노출 조건이 코드에 흩어지는데 어쩌지?

딜러 관리자 웹은 요금 변경 안내, 발신번호 변경 안내 같은 기간·조건부 팝업이 많다. 리뷰에서 “이 노출 조건 또 하드코딩이야?”가 반복됐다. 실제로 노출 날짜/시간/차량 브랜드 조건이 여러 커밋에 걸쳐 계속 바뀌었다.

여기서 배운 것:

  • 노출 조건(시작일·종료일·대상)은 컴포넌트 로직에서 분리해 데이터로 다룬다. 조건이 자주 바뀌는 걸 전제해야 한다.
  • “dev 상시 노출” 같은 환경별 분기가 필요하면 조건에 환경 플래그를 명시적으로 넣는다.
  • 공용 팝업 레이아웃과 개별 콘텐츠를 분리한다(레이아웃 재사용, 콘텐츠만 교체). 실제로 payment 팝업 콘텐츠와 공용 팝업 레이아웃을 분리하는 리팩토링을 했다.

정리

딜러 관리자 웹 리뷰에서 반복된 질문을 다섯 개로 묶으면 이렇다.

  1. 정적 문구는 CMS(노션)로 빼서 비개발자가 관리하게.
  2. 매 요청 외부 API를 때리지 말고 ISR로 캐시 + 장애 격리.
  3. 서버 시크릿은 import 경로와 NEXT_PUBLIC_ 규칙으로 클라이언트 유출 차단.
  4. CI 토큰은 최소 권한을 명시.
  5. 자주 바뀌는 노출 조건은 코드가 아니라 데이터로.

리뷰 질문을 정리해두면 다음 사람이 같은 질문에 다시 답하지 않아도 된다. 이게 리뷰 문서화의 진짜 이득이다.

관련 작업

  • 이용약관 노션 API 연동(ISR 자동반영) — getStaticProps + revalidate: 3600
  • GITHUB_TOKEN 권한 명시적 설정(최소 권한 원칙)
  • Payment popup content와 공용 popup layout 분리
  • 차량 brandId로 요금변경 팝업 노출 조건 분기
  • 이용약관 안내 팝업 노출 시간/날짜 조건 수정