산을 배경으로 한 한글 글꼴
📝 한줄 요약
같은 요약 표를 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. 추가 기능은 컴포넌트 안에서만 수정
// 두 화면 모두 자동으로 업데이트됨🔜 다음 계획
MeetingSummaryCard를 더 작은 컴포넌트로 분리 — BasisSelector, GranularityButtons를 각각 재사용 가능하게
Export 기능 추가 — 요약 표를 CSV/PDF로 다운로드, 컴포넌트에서 바로 가능하게
캐싱 최적화 — useMemo 대신 React Query로 서버 캐싱
솔직한 소감
처음에는 “Summary와 Report가 비슷한 건 알지만, 각 화면마다 코드를 별개로 쓰는 게 더 명확할 수도”라고 생각했어요.
그런데 Custom 기간을 추가할 때 한 곳만 수정했는데도 양쪽 화면에 나타났을 때, “아, 이게 좋은 설계구나”를 깨달았습니다.
컴포넌트 재사용이 코드 양을 줄이는 것보다, 버그 수를 줄이고 기능 추가를 빠르게 한다는 게 더 큰 이득이라는 걸 배웠어요.