공용 컴포넌트로 3개 화면 동기화하기

산을 배경으로 한 한글 글꼴

📝 한줄 요약

같은 요약 표를 Summary와 Report 탭에서 따로따로 만들려고 했는데, MeetingSummaryCard 하나의 컴포넌트로 통합하니, Custom 기간 필터를 한 번만 만들었는데도 양쪽 화면에 자동으로 들어갔습니다.

Before: Summary와 Report가 독립적인 코드 → 같은 기능도 중복 구현
After: 하나의 컴포넌트 → Props로 각 화면에 맞게 설정 → 자동 동기화

👥 이런 분께 추천

  • React에서 컴포넌트 재사용과 Props 설계를 배우고 싶은 분

  • 같은 기능이 여러 화면에서 필요할 때 효율적으로 처리하고 싶은 분

  • 컴포넌트 로직과 표시 로직을 분리하는 방법을 알고 싶은 분

  • DRY(Don’t Repeat Yourself) 원칙을 실전에서 적용하고 싶은 분

🙋 내 스펙 & 환경

항목

내용

직업

초과근무관리 앱 제작

코딩 경험

React / TypeScript / Supabase

개발 스택

Next.js 14 / Tailwind CSS

버전 관리

Git / Vercel 배포


진행 방법

1. 문제: 같은 표, 다르게 보여주기

Summary와 Report 탭이 요약 표를 필요로 했는데, 역할마다 봐야 하는 방식이 달랐어요:

회계 담당자 (Summary 탭):

  • 정산일(Confirmed Date) 기준으로만 봄

  • 주간 단위만 (회장부와 대조하기 위해)

  • 예: “8월 첫 주에 몇 건을 정산했는가?”

대표 (Report 탭):

  • 미팅 날짜(Calendar) 또는 정산일(Confirmed) 기준 전환 가능

  • 일/주/월 모두 선택 가능

  • Custom 범위도 지정 가능

처음엔 각각 따로 구현했습니다:

// SummaryView.tsx (회계용 요약)
const rows = data.filter(m => m.confirmedAt && ...)
// 직접 주간 필터링

// ReportView.tsx (대표용 요약)
const rows = data.filter(m => {
  if (basis === 'calendar') { ... }
  else { ... }
})
// 다시 작성한 필터링

문제: 필터링 로직이 두 곳에 있고, 버그가 있으면 두 곳을 고쳐야 합니다.

더 심한 건, Custom 기간을 추가할 때 양쪽 화면을 수정해야 했어요.

2. 첫 번째 시도: 함수 추출하기

공통 필터링 로직을 함수로 뺐어요:

// lib/summaryFilters.ts
function filterMeetingsByPeriod(
  meetings: Submission[],
  period: string,
  basis: 'confirmed' | 'calendar'
): Submission[] {
  // 날짜 기준으로 필터링
}

양쪽 컴포넌트에서 이 함수를 호출합니다. 좋지만, 여전히 결과를 렌더링하는 코드는 중복이었어요.

// SummaryView와 ReportView에서
const filtered = filterMeetingsByPeriod(data, ...);
const grouped = groupByStaffAndType(filtered);
// 요약 테이블 렌더링
// ... 이 렌더링이 두 곳에 있음

3. 해결: MeetingSummaryCard 컴포넌트 만들기

컴포넌트의 역할을 명확히 했어요:

  • 입력: 미팅 데이터, 필터 옵션

  • 처리: 필터링, 집계, 상태 관리

  • 출력: 요약 테이블

핵심 Props:

Props

역할

Summary

Report

rows

전체 미팅 데이터

전달

전달

lockedBasis

기준 고정

”confirmed”

없음 (사용자 선택)

granularityOptions

선택 가능한 단위

[“week”]

[“day”, “week”, “month”]

initialGranularity

처음 선택

”week"

"day”

4. 각 화면에서 사용하기

Summary (회계용):

// SummaryView.tsx
<MeetingSummaryCard
  rows={rows}
  title="Meeting Summary"
  description="회계 담당자가 정산일 기준으로 본 요약"
  lockedBasis="confirmed"  // 고정: 정산일
  granularityOptions={["week"]}  // 선택지: 주간만
  initialGranularity="week"
/>

Report (대표용):

// ReportView.tsx
<MeetingSummaryCard
  rows={rows}
  title="Detailed Analytics"
  description="대표가 기간과 기준을 자유롭게 선택"
  // lockedBasis 없음 = 사용자가 선택
  granularityOptions={["day", "week", "month"]}  // 선택지: //
  initialGranularity="day"
/>

결과:

  • Summary: 정산일 기준만, 주간 고정

  • Report: 미팅일/정산일 선택 가능, 일/주/월/커스텀 모두 가능

5. Custom 기간 추가 (버그 없이!)

기간 필터에 Custom 옵션을 추가할 때, MeetingSummaryCard 안의 PeriodPicker 컴포넌트를 수정했습니다.

Custom이 추가된 그 순간, Summary와 Report 양쪽에 자동으로 Custom 옵션이 나타났어요.

별도 수정 필요 없음:

  • Summary: lockedBasis=“confirmed”이고 granularityOptions=[“week”]이니 Custom이 무시됨 (좋음)

  • Report: Custom이 자동으로 사용 가능 (좋음)

6. 검증

테스트 시나리오 1: Summary에서 주간 선택

  • Week 버튼 클릭

  • 정산일 기준으로 이번 주 데이터만 표시 ✓

  • Basis 버튼 없음 (고정되어 있으므로) ✓

테스트 시나리오 2: Report에서 기준 전환

  • Calendar 기준 → 미팅 날짜 기준으로 표시 ✓

  • Confirmed 기준 → 정산일 기준으로 표시 ✓

  • Summary는 영향 없음 (lockedBasis=“confirmed”이므로) ✓

테스트 시나리오 3: Report에서 Custom 범위 지정

  • Custom 클릭

  • 달력에서 08/01 ~ 08/15 선택

  • 그 기간의 데이터만 표시 ✓

  • Summary는 여전히 주간 단위 ✓


🛠 사용한 도구

도구

역할

React Props

컴포넌트 재사용 시 설정 전달

TypeScript Generics

Submission[] 타입으로 유연하게

useMemo

필터링/집계 결과 캐싱 (불필요한 재계산 방지)

PeriodPicker

날짜 기반 필터 UI

Supabase

정산일, 미팅일 기준 쿼리


배운 점

💡 핵심 교훈

1. 컴포넌트 Props는 “제어점”이다

lockedBasis, granularityOptions 같은 Props를 잘 설계하면, 같은 컴포넌트가 여러 역할을 합니다. Props는 단순한 데이터 전달이 아니라 컴포넌트의 동작을 제어하는 인터페이스입니다.

2. “같은 기능”이 아니라 “같은 구조”를 찾기

Summary와 Report의 요약 표가 겉으로는 다르게 보이지만, 안에는 같은 구조(필터링 → 집계 → 테이블)가 있었어요. 이 구조를 찾아낸 게 핵심이었습니다.

3. 공용 컴포넌트의 성장통

처음엔 MeetingSummaryCard가 Record 필터에만 필요했어요. 그런데 Summary, Report까지 필요하니 점점 복잡해졌습니다. 그 과정에서 Props 설계가 중요함을 배웠습니다.

4. Custom 기능을 한 번만 만들기

Custom 기간을 추가할 때, MeetingSummaryCard와 PeriodPicker 안의 로직만 수정했습니다. 다른 화면들은 그냥 따라갔어요. 이게 좋은 컴포넌트 설계의 증거입니다.


적용할 점

🎯 개선 결과

항목

이전

이후

요약 테이블 정의

2곳

1곳

필터링 로직 정의

2곳

1곳

Custom 기능 추가 시 수정

2곳

1곳

새로운 화면에서 요약표 재사용

복잡

Props만 전달

📋 다시 쓸 수 있는 패턴

// 1. Props로 제어점을 명확히
interface MyComponentProps {
  data: T[];
  mode: 'locked' | 'flexible';
  options: string[];
  onSelect: (value: string) => void;
}

// 2. 각 화면에서 다른 설정으로 사용
// 화면 A
<MyComponent mode="locked" options={['a']} />

// 화면 B
<MyComponent mode="flexible" options={['a', 'b', 'c']} />

// 3. 추가 기능은 컴포넌트 안에서만 수정
// 두 화면 모두 자동으로 업데이트됨

🔜 다음 계획

  1. MeetingSummaryCard를 더 작은 컴포넌트로 분리 — BasisSelector, GranularityButtons를 각각 재사용 가능하게

  2. Export 기능 추가 — 요약 표를 CSV/PDF로 다운로드, 컴포넌트에서 바로 가능하게

  3. 캐싱 최적화 — useMemo 대신 React Query로 서버 캐싱


솔직한 소감

처음에는 “Summary와 Report가 비슷한 건 알지만, 각 화면마다 코드를 별개로 쓰는 게 더 명확할 수도”라고 생각했어요.

그런데 Custom 기간을 추가할 때 한 곳만 수정했는데도 양쪽 화면에 나타났을 때, “아, 이게 좋은 설계구나”를 깨달았습니다.

컴포넌트 재사용이 코드 양을 줄이는 것보다, 버그 수를 줄이고 기능 추가를 빠르게 한다는 게 더 큰 이득이라는 걸 배웠어요.

1
1개의 답글
밀어주고 끌어주는

온·오프라인 AI 스터디

AI로 어디까지 할 수 있는지
직접 확인하실 분만 신청하세요.