MinJun.

Moving (무빙)

이사 전문가와 사용자를 연결하는 매칭 플랫폼

기간

25. 11. 25. - 26. 01. 26.

역할

Team Lead, PM, Devops

기술 스택

TypeScriptNext.jsTanstack QueryZustandTailwindCSSVercelExpressPrismaPostgreSQLUbuntuNginxDockerGithub ActionsOpenAPIAWSJest

1. 프로젝트 개요

이사 과정에서는 견적 비교의 불편함, 정보 비대칭, 실시간 소통의 부재 등으로 인해 사용자가 합리적인 판단을 내리기 어렵다는 문제가 존재합니다.

저는 단순 기능 구현을 넘어, 실제 서비스 운영을 가정한 구조를 설계하는 것을 목표로 프로젝트를 진행했습니다.

2. 핵심 기여 및 기술적 판단

1. 비용과 운영을 고려한 서버 설계

프로젝트 초반, AWS EC2 기반 배포는 규모 대비 비용 부담이 크다고 판단했습니다.

단기 과제로 접근하지 않고 실제 운영 환경을 직접 설계해보고자 Ubuntu 기반 서버를 구축하고, Nginx + Docker 조합으로 배포 환경을 구성했습니다.

방화벽 설정, 포트 분리, 리버스 프록시, 환경 변수 보호 등을 직접 설계하며 개발과 운영은 다른 문제라는 점을 체감했습니다.

2. SSR 중심 렌더링 구조 재설계

Next.js를 사용했지만 초기 구현은 CSR 중심이었습니다. 이로 인해 hydration 비용 증가클라이언트 부담이 발생했습니다.

페이지의 역할에 따라 SSR/CSR을 명확히 분리하는 구조로 재설계했고, 가능한 많은 렌더링 책임을 서버로 이동시켰습니다.

이를 통해 초기 로딩 부담을 줄이고 클라이언트 리소스를 경량화할 수 있었습니다.

3. 데이터 집계 로직의 DB 레벨 이전

기사님의 리뷰 수, 평균 평점, 확정 견적 수 등은 여러 테이블에 분산되어 있었습니다.

초기에는 서비스 레이어에서 집계했지만, 정렬 및 필터링 구조에 한계가 있다고 판단했습니다.

DriverStatusView라는 View 테이블을 생성해 집계 로직을 DB 레벨로 이동했고, 데이터 조회 구조를 단순화하고 확장 가능하게 만들었습니다.

4. Docker 기반 표준화 및 CI/CD 자동화

서버 환경 차이수동 배포로 인한 오류 가능성을 줄이기 위해 백엔드를 Docker로 컨테이너화했습니다.

GitHub Actions + GHCR를 활용해 자동 빌드 및 배포 파이프라인을 구축했고, 디스코드 웹훅으로 배포 상태를 공유하도록 설계했습니다.

이를 통해 배포를 개인 작업이 아닌 팀이 신뢰할 수 있는 프로세스로 전환했습니다.

5. 대량 시드 데이터 검증

설계한 구조가 실제 환경에서도 유효한지 검증하기 위해 약 175만 건 규모의 시드 데이터를 주입했습니다.

초기에는 메모리 부하로 실패했지만, 배열 생성 및 INSERT 과정을 배치 처리로 분리해 최적화했고 로그 기반으로 상태를 추적하며 안정적으로 완료했습니다.

이를 통해 선택한 구조가 실제 데이터 환경에서도 견딜 수 있음을 검증했습니다.

3. 트러블 슈팅

175만 건 대량 시드 데이터 처리 중 메모리 초과

문제 상황

프로젝트 구조가 실제 운영 환경에서도 유효한지 검증하기 위해 약 175만 건 규모의 시드 데이터를 주입하는 테스트를 진행했습니다.

초기 구현에서는 모든 데이터를 배열에 생성한 뒤 한 번에 INSERT하는 방식을 사용했습니다.

그러나 실행 도중 서버 메모리 사용량이 급격히 증가했고, Node 프로세스가 중단되는 문제가 발생했습니다.

원인 분석

단순히 "데이터가 많아서"가 아니라, 데이터 생성과 INSERT가 모두 한 번에 이루어지면서 메모리 상에 대량 객체가 동시에 적재되는 구조가 문제라고 판단했습니다.

특히, 아래와 같은 구조적 문제가 있었습니다.

  • 배열 생성 단계에서 대규모 객체가 메모리에 유지됨
  • DB INSERT가 완료될 때까지 GC가 충분히 작동하지 않음
  • 로그 추적이 없어 어느 단계에서 병목이 발생하는지 명확하지 않음

해결 접근

저는 문제를 "코드 수정"이 아니라 "처리 방식 재설계"의 관점에서 접근했습니다.

1. 배치 처리 도입

  • 전체 데이터를 작은 단위(예: 1,000건)로 분리
  • 생성과 INSERT를 동일 단위로 반복 수행
  • 메모리 점유 시간을 최소화

2. 로그 기반 상태 추적

  • Winston 로그를 활용해 배치 단위 처리 상황을 기록
  • 어느 단계에서 병목이 발생하는지 추적 가능하도록 개선

결과

  • 메모리 초과 문제 없이 안정적으로 175만 건 데이터 주입 완료
  • 전체 처리 과정의 안정성 확보
  • 로그 기반으로 상태를 추적할 수 있는 구조 확립

배운 점

이 경험을 통해 "동작하는 코드""운영 가능한 구조"는 다르다는 점을 체감했습니다.

또한 성능 문제는 알고리즘보다 자원 사용 방식구조 설계의 문제일 수 있다는 것을 배웠습니다.

이후 기능 구현 시에도 처리 단위를 어떻게 나눌 것인지, 메모리와 DB 리소스를 어떻게 사용할 것인지 먼저 고민하게 되었습니다.

SSR 중심 렌더링 구조 재설계 (Hydration 비용 감소)

문제 상황

Next.js를 사용하고 있었지만, 초기 개발 과정에서는 React에 익숙한 방식대로 CSR 중심으로 화면을 구성하는 흐름이 많았습니다.

그 결과 초기 로딩 시점에 클라이언트에서 수행해야 할 JavaScript 실행과 hydration 범위가 커지면서, 불필요한 렌더링 비용이 발생한다고 판단했습니다.

특히 "단순히 동작하는 화면"은 빠르게 만들 수 있었지만, 서비스가 확장될수록 초기 로딩 부담이 커지고 UX에 영향을 줄 가능성이 높았습니다.

원인 분석

문제의 핵심은 Next.js의 기능 부족이 아니라, 렌더링 책임이 어디에 있는지(서버 vs 클라이언트)가 명확히 설계되지 않은 구조였습니다.

  • 페이지 단위로 SSR/CSR 기준이 정리되지 않아, 습관적으로 클라이언트 컴포넌트가 늘어남
  • 상호작용이 필요 없는 영역까지 클라이언트에서 렌더링되어 hydration 비용 증가
  • "편한 구현"을 우선하면 구조가 점점 CSR에 쏠리는 경향이 발생

즉, 성능 최적화는 기술 지식의 문제가 아니라 렌더링 책임을 재배치하는 설계 문제라고 결론냈습니다.

해결 접근

저는 "CSR을 줄이자"가 아니라, 페이지의 역할을 기준으로 렌더링 전략을 결정하는 방식으로 접근했습니다.

1. 페이지 성격 기준으로 SSR/CSR 분리 원칙 수립

  • 기본 전략: 가능한 많은 렌더링을 서버에서 처리
  • 예외: 입력/상태/이벤트 등 상호작용이 필요한 최소 영역만 클라이언트로 분리

2. 서버 컴포넌트 중심으로 구조 재설계

  • 대부분의 페이지를 서버 컴포넌트 기반으로 구성해 SSR 흐름을 강화
  • 클라이언트 컴포넌트는 "UI 상호작용이 필요한 영역"에 한정
  • 클라이언트가 담당해야 할 렌더링 범위를 의도적으로 축소

3. 빌드 결과를 기준으로 렌더링 전략 검증

  • 설계가 실제로 반영되는지 빌드 결과에서 정적/서버 렌더링 페이지를 구분해 점검
  • 페이지별 성격에 따라 렌더링 방식이 일관되게 선택되는지 확인하며 구조를 정리

결과

  • 불필요한 hydration 범위를 줄이며 초기 로딩 시 클라이언트 부담을 완화했습니다.
  • "모든 것을 CSR로 해결"하던 개발 습관에서 벗어나, 페이지 성격에 맞는 SSR/CSR 분리 설계를 통해 UX 중심의 렌더링 구조를 확보했습니다.

배운 점

이번 경험을 통해 프론트엔드 성능 최적화는 "특정 기술을 많이 아는 것"이 아니라, 이 페이지가 어떤 역할을 하는지 정의하고, 렌더링 책임을 어디에 둘지 판단하는 과정임을 체감했습니다.

이후 프론트엔드를 설계할 때도 서버가 책임져야 하는 것, 클라이언트가 책임져야 하는 것을 먼저 구분하고, 기능 구현보다 구조적 선택을 우선하는 기준을 갖게 되었습니다.

기타 트러블 슈팅

Ubuntu 서버 구축 중 운영 환경 설계 이슈

AWS 기반 배포비용 대비 효율이 낮다고 판단했습니다.

Ubuntu 서버를 직접 구축하며 방화벽, 포트 분리, 리버스 프록시를 설정했고, 개발과 운영이 다른 문제임을 체감했습니다.

이후 모든 기술 선택에서 "운영 환경에서도 유효한가?"를 기준으로 판단하게 되었습니다.

Docker 도입 전 환경 차이 문제

팀원별 실행 환경 차이로 예기치 않은 오류가 발생했습니다.

백엔드를 Docker로 컨테이너화실행 환경을 표준화했고, GitHub Actions 기반 CI/CD를 구축해 배포를 자동화했습니다.

이를 통해 배포를 개인 작업이 아닌 팀이 신뢰할 수 있는 프로세스로 전환했습니다.

OpenAPI 기반 타입 불일치 문제

프론트엔드와 백엔드가 동시에 개발되며 API 스펙 변경타입 불일치 위험이 있었습니다.

Swagger를 단일 기준으로 삼고 openapi-typescript를 도입해 타입을 자동 생성하도록 구조화했습니다.

타입 불일치를 개인의 주의가 아닌 시스템으로 방지하는 방식으로 전환했습니다.

DriverStatusView 설계 전 집계 로직의 한계

기사 통계 데이터서비스 레이어에서 집계하던 구조는 정렬 및 필터링에 한계가 있었습니다.

집계 로직을 DB View 테이블로 이전조회 구조를 단순화하고 확장성을 확보했습니다.

이를 통해 데이터 처리 범위를 애플리케이션에서 데이터베이스까지 확장해 사고하게 되었습니다.

위치 기반 요청 기능 구현

요구사항에는 없었지만, 기사님 입장에서 가장 중요한 정보는 "가까운 요청"이라고 판단했습니다.

GeocodingHaversine 알고리즘을 활용해 거리 기반 요청 조회 기능을 구현했습니다.

기능 구현을 넘어 사용자 의사결정을 돕는 구조를 설계하는 경험이 되었습니다.

로깅 체계 부재 문제

문제 발생 시 감에 의존해 원인을 추측해야 하는 상황이 반복되었습니다.

Winston을 도입해 로그 레벨을 분리하고 파일 기반 로깅 체계를 구축했습니다.

기술적 판단을 감이 아닌 근거 기반으로 검증할 수 있는 환경을 마련했습니다.

4. 결과 및 회고

이 프로젝트에서 가장 어려웠던 점은 기능 구현이 아니라, 문제를 객관적으로 바라보고 가장 합리적인 선택을 반복하는 과정이었습니다.

운영, 성능, 협업 안정성까지 고려하며 설계를 진행한 경험을 통해 기술은 구현 능력이 아니라 판단의 연속이라는 점을 배웠습니다.

이후 프로젝트에서도 항상 "이 선택이 운영 환경에서도 괜찮은가?"를 기준으로 기술을 판단하고 있습니다.

5. 팀원들의 피어 리뷰

팀원들이 실제로 작성해준 피어 리뷰입니다.

[강점]

  • 팀장으로서 리더십 있게 팀원들을 잘 이끌어주시고, 솔선수범하여 굳은 일 마다하지 않고 실천해주셔서 너무 감사했습니다.
  • 팀 일정 관리 능력이 뛰어나다.
  • 팀 내에서든 외에서든 소통을 잘 하신다. 회의 많이 하고 팀원 의견도 많이 물어봐 주셔서 감사하다.
  • 이번 프로젝트 고도화 보면 개발 속도도 엄청 빠른 것 같다.
  • 발표 준비부터 발표까지 수고 많으셨습니다.
  • 팀원 역할 배분부터 프로젝트 전반의 진행사항을 잘 이끌어주셔서 감사합니다.
  • 팀원의 분량을 잘 챙겨주시려고 하시는 모습이 감사했습니다.

[보완해야 할 점]

  • 이미 완성된 개발자라는 자부심을 가져도 되니 자신감 있게 도전하는 분이 되시면 좋겠습니다.
  • X
  • 이미 정말 잘하고 계신 것 같습니다.

6. 기술 스택

Frontend

  • Next.js

    서버 컴포넌트와 SSR을 활용해 초기 로딩 성능을 개선하고, 렌더링 책임을 서버 중심으로 설계하기 위해 선택했습니다.

  • TypeScript

    프론트엔드–백엔드 간 타입 안정성을 확보하고, 협업 과정에서 발생할 수 있는 런타임 오류를 사전에 방지하기 위해 사용했습니다.

  • TanStack Query

    서버 상태 관리를 구조화하고, 데이터 캐싱·재요청 제어를 통해 클라이언트 성능과 사용자 경험을 개선하기 위해 도입했습니다.

Backend

  • Node.js / Express

    경량 구조로 빠르게 API 서버를 설계하고, 미들웨어 기반 확장성과 유연성을 확보하기 위해 선택했습니다.

  • Prisma

    타입 안정성을 유지하면서 데이터베이스 스키마와 애플리케이션 레이어를 명확히 연결하기 위해 사용했습니다.

  • PostgreSQL

    관계형 데이터 구조와 집계 처리(View, Index 등)를 활용해 복잡한 조회 요구사항을 효율적으로 처리하기 위해 선택했습니다.

Infra / DevOps

  • Ubuntu

    비용을 고려하면서도 실제 운영 환경을 직접 설계해보기 위해 온프레미스 기반 서버 환경을 구축했습니다.

  • Docker

    실행 환경을 표준화해 팀원 간 환경 차이를 제거하고, 향후 클라우드 마이그레이션을 대비하기 위해 도입했습니다.

  • Nginx

    리버스 프록시 및 트래픽 관리, 보안 설정을 위해 활용했습니다.

  • GitHub Actions / GHCR

    코드 변경 시 자동 빌드 및 배포가 이루어지는 CI/CD 파이프라인을 구축해 배포 안정성을 확보했습니다.