개발 slecs

대시보드 차트·CRUD API·렌더러를 한 번에 연결한 주간 작업

목차

Week 4 작업이 마무리됐다. renderer 계층에 user-app을 붙이고, 데이터 CRUD API를 연결하고, 대시보드 차트까지 한 번에 밀어넣은 주였음. 커밋 하나에 묶였지만 실제로는 꽤 여러 레이어를 건드린 작업이었다.


왜 이 세 가지를 같이 묶었나

솔직히 말하면, 처음에는 renderer / API / 차트를 따로따로 PR로 쪼개려고 했다. 근데 막상 작업하다 보니 세 가지가 서로 너무 강하게 물려 있어서 분리하는 게 오히려 비용이었음.

  • renderer가 없으면 차트 컴포넌트를 어디서 렌더링할지 결정이 안 됨
  • CRUD API가 확정되지 않으면 차트에서 쏠 쿼리 구조가 흔들림
  • 차트 없이 renderer만 올리면 팀 입장에서 "이게 동작하는 건지" 확인이 불가능

결과적으로 하나의 커밋에 묶은 건 맞는 선택이었다고 본다. 다만 리뷰어 입장에서는 컨텍스트 전환이 많은 PR이라 리뷰 코멘트를 어디에 달아야 할지 헷갈릴 수 있었음. 다음에 이런 상황이 오면 커밋은 묶되, PR description에 레이어별 변경 요약을 테이블로 정리해두는 게 낫겠다 싶었다.


파일별로 무슨 역할을 했나

파일 역할 이번 변경 포인트
apps/web/package.json 의존성 선언 차트 라이브러리 등 신규 패키지 추가
[...path]/page.tsx 동적 라우트 (required segment) user-app renderer 진입점 연결
[[...path]]/page.tsx 동적 라우트 (optional segment) 경로 없을 때 fallback 처리
auth-helpers.ts 서버 사이드 인증 유틸 CRUD API 호출 시 세션 검증 로직 추가
data-validation.ts 입력값 검증 CRUD 요청 페이로드 스키마 정의
DashboardPage.tsx 대시보드 렌더러 차트 컴포넌트 조립 및 데이터 바인딩

Next.js App Router에서 [...path][[...path]]를 같이 쓰는 구조가 처음 보는 사람한테는 헷갈릴 수 있다. required catch-all이랑 optional catch-all을 각각 따로 파일로 분리한 건, orgSlug + projectSlug 아래에서 "경로가 있는 경우"와 "경로가 없는 경우"의 렌더링 로직을 명확하게 나누기 위해서였음. 하나로 합치면 페이지 컴포넌트 내부에서 분기가 늘어나서 오히려 지저분해진다.


renderer 설계에서 고민했던 부분

DashboardPage.tsxapps/web/src/server/renderer/ 아래에 둔 게 의도적인 선택이었다. 이 컴포넌트는 클라이언트에서 interactive하게 동작하는 부분도 있지만, 기본적으로 서버에서 데이터를 가져와서 레이아웃을 조립하는 책임을 갖기 때문에 server 디렉토리 안에 위치시켰음.

// DashboardPage.tsx (간략 패턴)
export async function DashboardPage({ orgSlug, projectSlug, path }: Props) {
  const session = await validateSession(); // auth-helpers
  const data = await fetchDashboardData({ orgSlug, projectSlug, path });

  return (
    <DashboardLayout>
      <ChartGrid data={data} />
    </DashboardLayout>
  );
}

데이터 페칭과 렌더링 책임이 한 컴포넌트에 몰리지 않도록 fetchDashboardData 같은 레이어를 바깥으로 빼두었음. 이렇게 해두면 나중에 차트 컴포넌트만 교체하거나, 데이터 소스만 바꿀 때 영향 범위가 최소화된다.


auth-helpers, data-validation 건드린 이유

CRUD API를 새로 추가하면서 두 파일을 건드리게 됐는데, 이게 이번 작업의 숨겨진 핵심이었다.

기존 auth-helpers.ts는 페이지 렌더링 시 세션 체크 용도로만 쓰이고 있었음. 근데 CRUD API route handler에서도 동일한 세션 검증이 필요해지니까, 로직을 헬퍼로 올려두지 않으면 API마다 같은 코드를 복붙하게 된다. 그래서 이번에 공통 유틸로 끌어올렸음.

data-validation.ts도 마찬가지다. CRUD 요청이 들어올 때 페이로드를 검증하는 스키마를 여기에 집중시켜 뒀음. 팀 전체가 "검증은 여기서 한다"는 약속을 공유하면, 나중에 새 API를 추가하는 사람도 자연스럽게 이 파일을 참조하게 된다.

// data-validation.ts 패턴
export const createRecordSchema = z.object({
  projectSlug: z.string().min(1),
  payload: z.record(z.unknown()),
});

export function validateCreateRecord(input: unknown) {
  return createRecordSchema.safeParse(input);
}

팀에 새로운 API를 작성하는 사람이 생겼을 때 "검증 어디서 해요?"라는 질문이 안 나오도록 파일 위치와 패턴을 먼저 만들어두는 게 총괄 역할이라고 생각함.


Week 4 회고

이번 주는 코드량보다 레이어 간 계약을 잡는 데 더 많은 시간이 들었다. renderer가 어디까지 책임지고, API가 어디서 끊기고, 검증이 어디서 일어나는지. 이걸 팀원들이 명시적으로 읽을 수 있게 파일 구조와 네이밍으로 표현하는 것.

차트가 화면에 뜨는 순간 팀에서 "오 이제 되는 거네"라는 반응이 나왔는데, 그게 이번 주 작업의 가시적인 결과물이었다. renderer 계층이랑 API 쪽은 눈에 안 보이는 기반 작업이라 체감이 덜하지만, 이게 없으면 차트도 안정적으로 얹힐 수 없었음.

끝.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.