대규모 React Native 앱에서 클라이언트 측 엔터티 정규화가 실제로 필요해지는 경우

발행: (2026년 1월 9일 오후 09:12 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

배경

지난 몇 년간 여러 React Native 프로젝트—다양한 제품, 다양한 팀—를 진행하면서 매우 비슷한 증상을 계속해서 마주했습니다.

앱이 성장함에 따라 동일한 엔티티가 점점 더 많은 곳에서 나타나기 시작했습니다:

  • 피드
  • 상세 화면
  • 검색 결과
  • 알림
  • 백그라운드 업데이트

각 새로운 기능은 작지만 합리적인 결정을 도입했습니다:

  • 리스트 응답을 캐시
  • 화면 포커스 시 재조회
  • 부분 업데이트 병합
  • 파생 셀렉터 추가
  • 화면 간 데이터 수동 동기화

개별적으로는 이 선택들이 의미가 있었습니다. 하지만 전체적으로 보면 모두 같은 근본적인 문제를 해결하려는 시도였습니다. 어느 순간 이것이 우연이 아니라는 것이 명확해졌습니다.

정규화가 불필요해 보였을 때

오랫동안 엔티티 정규화는 다음과 같은 경우에 불필요해 보였습니다:

  • 단일 화면에만 속함
  • 수명이 짧음
  • 다른 곳에서 재사용되지 않음

이러한 경우에는 API 응답 데이터를 그대로 유지하는 것이 완벽히 작동하고, 정규화를 적용하면 큰 이득 없이 오히려 절차가 늘어납니다.

정규화가 필요해지는 순간

데이터가 화면‑로컬을 벗어나기 시작하면서 문제가 발생했습니다. Redux Toolkit이나 React Query와 같은 라이브러리는 좋은 이유가 있어 인기가 있지만, 그 강점이 바로 범위가 넓다는 점입니다.

내 핵심 요구사항은 훨씬 좁았습니다: 스크린 간 공유 데이터에 대한 반응형 업데이트와 안정적인 정체성.

  • MobX가 그 부분을 매우 잘 처리했습니다.

  • 나머지 아키텍처는 이후에 비교적 표준적인 클린‑코드 원칙을 따라 자연스럽게 형성되었습니다:

    • 중복된 엔티티 인스턴스를 피하기 위한 정규화
    • 중첩된 DTO 트리 대신 명시적인 관계
    • 장기 데이터에 대한 라이프사이클 경계
    • 레이스 컨디션을 방지하기 위한 비동기 오케스트레이션

이 모든 것이 사전에 계획된 것은 아니었습니다. 시간이 지나면서 코드베이스는 기능보다 조정 로직이 더 많이 늘어났습니다.

데이터‑라이프사이클 문제

엔티티가 더 이상 단일 화면에만 속하지 않게 되었습니다. 명시적인 규칙이 없으면 다음과 같은 문제가 발생했습니다:

  • 명확한 제거 전략이 없어 메모리 증가
  • 잊혀진 참조로 인한 우발적 보존
  • 누가 실제로 데이터를 “소유”하는지에 대한 불확실성

라이프사이클을 명시적인 관심사로 만들면서 이러한 문제들을 명확하고 조합 가능한 형태로 전환했습니다:

  • 가비지 컬렉션 전략
  • 영속성
  • 비동기 제어 (취소, 재시도, 새로 고침)
  • 통합 경계

이를 플러그인 가능한 레이어로 다루면서 책임이 명확해졌습니다.

접근 방식 추출

이 시점에서 구조가 단일 프로젝트에 묶여 있지 않게 되었습니다. 이 접근 방식을 작은 라이브러리로 추출하는 것이 원래 목표는 아니었지만, 같은 구조가 여러 프로젝트에서 반복되면서 자연스럽게 이루어졌습니다. 아직도 실험 단계입니다.

궁금하시다면, 이 접근 방식을 작은 오픈‑소스 실험으로 정리했습니다:

https://github.com/nexigenjs/entity-normalizer

열린 질문

여기에 정답이 하나만 있다고 생각하지는 않습니다. 여러분은 오늘날 어떻게 접근하고 계신가요?

  • 클라이언트‑사이드 엔티티 정규화가 언제부터 가치를 발휘하기 시작했나요?
  • 서버 캐시와 도메인 엔티티 사이의 경계는 어디에서 그리나요?
  • 공유 클라이언트‑사이드 데이터의 라이프사이클과 소유권을 어떻게 관리하고 있나요?
Back to Blog

관련 글

더 보기 »

고트래픽 Node.js API 최적화 전략

Node.js의 이벤트‑드리븐 아키텍처를 활용하세요. I/O 작업을 논블로킹으로 유지하고, 블로킹 코드 대신 async/await 또는 promises를 사용합니다. javascript // Use async…