0377

개발

[re-log] 대시보드 페이지 리팩토링으로 배우는 모듈화와 유지보수성

| 서론

안녕하세요 팡일입니다.

오늘은 관리자 페이지에 사용되는 대시보드 페이지의 리팩토링에 대해서 다뤄보려고 합니다. 처음에는 간단한 통계 정보를 보여주기 위해 빠르게 구현했던 페이지가, 어느새 데이터 페칭, 데이터 가공, 차트 렌더링, UI 마크업 등 너무 많은 책임을 한 파일에 떠안고 있었습니다. 이대로는 앞으로 기능을 추가하거나 코드를 수정하기가 매우 어렵겠다는 판단을 내릴 수밖에 없었습니다.

이 포스트에서는 제가 해당 Svelte 페이지를 어떻게 분석하고, 어떤 원칙을 가지고 리팩토링을 진행했으며, 그 결과 코드가 어떻게 더 건강하게 변화했는지에 대한 여정을 공유하고자 합니다.

| 문제 인식: 거대 컴포넌트의 그림자 (As-Is)

리팩토링 전, /routes/(private)/manage/+page.svelte 파일은 다음과 같은 문제점들을 안고 있었습니다.

1) 과도한 책임 (Poor Separation of Concerns)

  • 데이터 페칭
    : onMount에서 getAllUsers, getAllRetrospects 서비스를 직접 호출했습니다.

  • 데이터 가공
    : 페칭한 데이터를 groupDataByDay라는 내부 함수를 통해 차트에 맞는 형태로 가공했습니다.

  • UI 렌더링
    : 총 사용자 수, 총 회고 수를 표시하는 카드 UI 마크업이 페이지 안에 그대로 존재했습니다.

  • 외부 라이브러리 제어
    : Chart.js 라이브러리를 직접 초기화하고, bind:this를 이용해 <canvas> DOM 요소를 제어하는 로직이 컴포넌트 내부에 깊숙하게 섞여 있었습니다.

2) 낮은 재사용성 (Low Reusability)

: 통계 수치를 보여주는 ‘카운트 카드’나 ‘차트 카드’와 같은 UI 요소들은 페이지 내에서 반복적으로 사용되거나, 혹은 구조적으로 분리되어 관리될 필요가 있었습니다. 하지만 모든 로직이 하나의 페이지 파일 내에 강하게 결합되어 있어, 이러한 중복되는 요소들을 효율적으로 관리하고 재사용하기 어려운 구조였습니다.

3) 어려운 유지보수 (Difficult Maintenance)

“차트 색상을 파란색에서 빨간색으로 바꿔주세요”라는 단순한 요청이 있다고 가정해봅시다. 그렇다면, 거대한 파일을 열어 Chart.js 설정 코드가 어디 있는지부터 찾아야 합니다. UI, 데이터 로직, 라이브러리 설정이 뒤엉켜 있어 작은 변경조차도 예상치 못한 사이드 이펙트를 일으킬 위험이 컸습니다.

| 목표 설정: 역할과 책임을 나누다 (To-Be)

극복하기 위해 다음과 같은 명확한 목표와 설계 원칙을 세웠습니다.

  • 원칙 1: 단일 책임 원칙(SRP)
    컴포넌트와 모듈은 오직 하나의 책임만 가져야 한다.

  • 원칙 2: 컴포넌트 기반 아키텍처
    UI를 독립적이고 재사용 가능한 작은 단위로 나눈다.

  • 원칙 3: 관심사의 분리(SoC)
    ‘무엇을 보여줄 것인가(Data)’와 ‘어떻게 보여줄 것인가(View)’를 명확히 분리한다.

이 원칙을 기반으로 다음과 같은 새로운 구조를 구상했습니다.

  1. 페이지 컴포넌트 (+page.svelte)
    데이터 페칭 및 데이터 흐름만 관리하는 ‘컨테이너’ 역할만 수행합니다.

  2. 표현 컴포넌트 (CountCard.svelte, ChartCard.svelte)
    순수 UI 렌더링에만 집중하는 ‘프레젠테이션’ 컴포넌트입니다.

  3. 유틸리티 모듈 (/utils/chart)
    데이터 가공(groupDataByDay)이나 Chart.js 제어(renderChart)와 같은 순수 로직을 담당합니다.

  4. 타입 정의 파일 (/types/interfaces)
    UserDoc, RetrospectDoc 등의 타입을 관리하는 ‘단일 진실 공급원’ 역할을 합니다.

| 실행: 점진적인 리팩토링 여정

1) 1단계: 타입 중앙화 및 강화

기존에 페이지 내부에 임시로 정의되어 있던 UserDoc과 RetrospectDoc 타입을 src/types/interfaces/로 이동시켜 명확한 모델로 정리했습니다. createdAt과 같은 중요한 필드는 옵셔널에서 필수로 바꿔 데이터 안정성을 강화했습니다.

2) 2단계: 순수 로직 분리 (유틸리티 추출)

+page.svelte 내부에 있던 groupDataByDay와 renderChart 함수를 src/lib/utils/chart/ 아래로 각각 분리했습니다.

  • groupDataByDay.ts
    : Firestore 데이터 배열을 날짜별 카운트 Map으로 만드는 순수 함수.

  • renderChart.ts
    : Chart.js 초기화 및 설정을 캡슐화하여, 다른 컴포넌트들이 내부 구현을 알 필요 없도록 구조화.

3) 3단계: 표현 컴포넌트 생성

UI 패턴을 기반으로 다음 재사용 가능한 컴포넌트를 제작했습니다.

  • CountCard.svelte: 통계 숫자를 보여주는 카드

  • ChartCard.svelte: Chart.js 렌더링을 전담하는 카드

4) 4단계: 페이지 컴포넌트 재구성

: 기존 +page.svelte에서는 내부 함수·차트 설정·UI 마크업이 모두 섞여 있었습니다. 이를 제거하고 다음처럼 간결한 구조로 변경했습니다.

(1) Before

code
<script>
    // ... 데이터 페칭, 차트 인스턴스, 유틸 함수 등등 ...
</script>

<!-- 복잡한 div와 canvas 마크업 -->
<div class="card">
    <h2>총 사용자 수</h2>
    <p>{totalUsers}</p>
</div>
<div class="card">
    <canvas bind:this={usersCanvas}></canvas>
</div>

(2) After

code
<script>
    import CountCard from '...';
    import ChartCard from '...';
    import { groupDataByDay } from '...';

    // ... 데이터 페칭 로직만 남음 ...
</script>

<CountCard cardTitle="총 사용자 수" count={totalUsers} suffix="명" />
<ChartCard cardTitle="일일 가입자 수" data={usersByDay} />

| 결과: 이를 통해 얻은 것들

이번 리팩토링을 통해 다음과 같은 실질적인 이점을 얻었습니다.

  • 가독성과 유지보수성 향상
    페이지는 이제 전체 구조가 한눈에 들어오는 형태가 되었고, 각 파일이 맡은 책임이 명확해졌습니다.

  • 재사용성 극대화
    CountCard와 ChartCard는 프로젝트 전반에서 독립적인 ‘부품’으로 활용할 수 있게 되었습니다.

  • 테스트 용이성 증가
    복잡한 UI와분리된 순수 함수 덕분에 핵심 비즈니스 로직을 보다 안정적으로 단위 테스트할 수 있는 환경이 갖춰졌습니다.

| 마치며

소프트웨어의 품질은 단순히 기능이 돌아간다는 것만으로 결정되지 않습니다. 미래의 변화에 얼마나 유연하게 대응할 수 있는지도 중요한 기준입니다. 이번 리팩토링은 Svelte의 컴포넌트 기반 철학을 이해하고, 장기적으로 확장 가능한 구조를 구축하는 의미 있는 과정이었습니다.

‘돌아가는’ 코드에 만족하지 않고, ‘더 나은’ 코드를 위해 끊임없이 고민하는 것. 그것이 개발자로서 성장하는 길이라는 생각을 다시 한 번 확인할 수 있었습니다.