📚FOS Study
홈카테고리
홈카테고리

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agents 페이지로 이동
    • langgraph 페이지로 이동
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • architecture 페이지로 이동
    • [초안] 시니어 백엔드를 위한 API 설계 실전 스터디 팩 — REST · 멱등성 · 페이지네이션 · 버전 전략
    • 캐시 설계 전략 총정리
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 1,600만 고객과 올영세일을 버티는 설계
    • [초안] MSA 서비스 간 통신: Redis Cache-Aside × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] Template Method Pattern — 처리 골격을 고정하고 변형은 서브클래스에 맡기는 설계 전략
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • css 페이지로 이동
    • FlexBox 페이지로 이동
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • redis 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • 커넥션 풀 크기는 얼마나 조정해야할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • monitoring 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
  • go 페이지로 이동
    • Go 언어 기본 학습
  • http 페이지로 이동
    • HTTP Connection Pool
  • interview 페이지로 이동
    • 210812 페이지로 이동
    • company-analysis 페이지로 이동
    • experience-based 페이지로 이동
    • master 페이지로 이동
    • 뱅크샐러드 AI Native Server Engineer
    • CJ 올리브영 커머스플랫폼유닛 Back-End 개발 지원 자료
    • 마이리얼트립 - Platform Solutions실 회원주문개발 Product Engineer
    • NHN 서비스개발센터 AI서비스개발팀
    • nhn gameenvil console backend 직무 인터뷰 준비
    • 면접을 대비해봅시다
    • 토스증권 Server Developer (Platform) 지원 자료
    • 토스증권 Server Developer (Product) 지원 자료
    • 토스뱅크 Server Developer (Product) 지원 자료
    • Tossplace Node.js Developer
    • 토스플레이스 Node.js 백엔드 컬처핏
  • java 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • OpenTelemetry 란 무엇인가?
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • Data_Structures_and_Algorithms 페이지로 이동
    • Heap 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node.js
    • npm vs pnpm 선택기준은 무엇인가요?
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 기본
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • observability 페이지로 이동
    • [초안] Datadog APM 실전 투입 가이드: Java/Spring 서비스 관측성 스택 구축하기
  • react 페이지로 이동
    • JSX 페이지로 이동
    • VirtualDOM 페이지로 이동
    • v16 페이지로 이동
  • resume 페이지로 이동
    • 지원 문항
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, JMH, Contract까지
📚FOS Study

개발 학습 기록을 정리하는 블로그입니다.

바로가기

  • 홈
  • 카테고리

소셜

  • GitHub
  • Source Repository

© 2025 FOS Study. Built with Next.js & Tailwind CSS

목록으로 돌아가기
📁task/ sb-dev-team

13개 로케일 다국어 시스템 — Svelte derived 합성 + 백엔드 캐시 사전 구성

약 6분
2026년 4월 19일
GitHub에서 보기

13개 로케일 다국어 시스템 — Svelte derived 합성 + 백엔드 캐시 사전 구성

진행 기간: 2023.08 ~ 2024.02

스포츠 베팅 플랫폼의 다국어 시스템을 프론트엔드부터 백엔드 캐시까지 설계·구현했다. 글로벌 대응을 위해 13개 로케일을 지원했고, 스포츠 베팅이라는 도메인 특성상 UI 문구뿐 아니라 경기 마켓 이름, 선수 이름 치환, 핸디캡 표기 같은 템플릿 번역까지 필요했다.


요구사항이 만든 제약

일반 웹 서비스의 i18n과는 결이 달랐다.

  • 13개 로케일을 런타임에 전환 — 페이지 리로드 없이 즉시 반영
  • 두 가지 메시지 소스 — 앱 UI 문구(내부 관리)와 경기 용어(Betradar 같은 외부 스포츠 데이터 공급사 제공)가 별개
  • 템플릿 치환 — "{$competitor1} vs {$competitor2}"처럼 선수/팀 이름을 실시간 경기 데이터와 합성
  • 핫 리로드 — 운영 중 어드민에서 번역을 수정하면 앱 재배포 없이 즉시 반영
  • 로케일 의존 파싱 — 일본어는 괄호가 전각(()이라 마켓 이름 정리 정규식이 다르다

이 조합이면 svelte-i18n 같은 라이브러리 하나로는 부족했다. Svelte derived store 기반으로 직접 구성했다.


전체 구조

[어드민] ─ MQ(정적 데이터 리로드) ─▶ [백엔드 캐시 리로드]
                                 │
[DB: 언어 / 외부 벤더 언어 테이블]
                                 │
                        [백엔드: 13 로케일 × N 키 맵 사전 구성]
                                 │
                          GET /api/lang/{locale}
                                 │
                                 ▼
      [프론트: LANG_STORE (writable)] ← init(data)
                 │
                 ▼
  [LanguageService: derived 체인]
      └─▶ 템플릿 치환 derived 여러 개 (선수 이름, 핸디캡, outcome)
           └─▶ 상위 합성 derived (marketName, outcomeName …)
                 │
                 ▼
   [컴포넌트: $message('key') — 언어 변경 시 자동 리렌더]

응답 시점 계산을 사전 계산으로 밀어 넣는다는 방향이 양쪽에 공통이다. 백엔드는 캐시 빌드 시점에, 프론트는 derived 그래프 빌드 시점에 계산을 끝낸다.


프론트엔드 — Svelte derived로 반응형 다국어

언어 데이터 store

두 소스(system/외부 벤더)를 하나의 writable에 묶었다.

// 개념 설명용 의사코드
type LangData = {
  vendor: Record<string, string>,   // Betradar 같은 외부 공급사 용어
  system: Record<string, string>,   // 앱 UI 문구
}

export const LANG_STORE = writable<LangData>({ vendor: {}, system: {} })

묶은 이유는 단순하다. 언어 변경은 항상 두 맵을 같이 교체한다. 따로 관리하면 둘 중 하나만 갱신된 중간 상태가 UI에 노출될 여지가 있다.

derived를 "함수를 반환하는 store"로 쓴다

핵심 트릭이다. derived가 값이 아니라 (key) => string 함수를 반환하게 만든다.

// 개념 설명용 의사코드
export const message = derived(
  LANG_STORE,
  $store => (key, ...args) => interpolate($store.system[key] ?? key, ...args)
)

컴포넌트에서 $message('login.title')로 쓴다. store가 바뀌면 message 자체가 재구성되고, 이 함수를 호출하는 모든 컴포넌트가 자동 재평가된다.

이 한 줄이 다국어 시스템의 반응성 전부를 담고 있었다. "언어 데이터 + 호출 시 파라미터"를 derived의 클로저 + 반환 함수로 분리하는 덕분에 언어 변경 1번이 모든 소비처에 전파된다.

템플릿 치환 — derived의 합성

스포츠 베팅 마켓 이름은 "Over {total}", "Handicap {$competitor1} +{hcp}" 같은 템플릿. 로케일이 바뀌면 템플릿이 바뀌고 거기에 실시간 경기 데이터가 합성된다. derived를 여러 단계로 쌓아서 풀었다.

// 개념 설명용 의사코드
export const replaceCompetitors = derived(
  vendorMessage,
  $vendor => (template, match) =>
    template
      .replace('{$competitor1}', $vendor(match.homeId, match.homeName))
      .replace('{$competitor2}', $vendor(match.awayId, match.awayName))
)

// 상위 합성
export const marketName = derived(
  [vendorMessage, replaceCompetitors],
  ([$vendor, $replace]) => (key, defaultValue, match) => { /* ... */ }
)

실제로는 이런 합성 derived가 6~7개 있다(marketName, outcomeName, 변형 몇 개, highlight 등). 전부 LANG_STORE에 궁극적으로 의존하니 언어 변경 1번이 그래프 전체를 자동 갱신한다.

인사이트. derived 합성은 "의존 그래프"를 선언적으로 표현한다. 새 치환 함수를 추가해도 그래프 끝에 노드 하나 달면 된다. 명령형이었다면 "A 갱신, 그다음 B 갱신..." 같은 순서 관리 코드가 붙었을 것이다.

미번역 키 감지

치환 후에도 {...} placeholder가 남아 있으면 raw가 화면에 노출된다. 모든 derived 끝에 hasLeftoverPlaceholder 가드를 붙여 남아 있으면 defaultValue로 fallback. 번역 키 누락이나 placeholder 불일치가 있어도 깨진 문자열이 직접 노출되진 않는다.


백엔드 — 응답 시점 계산을 캐시 시점으로

백엔드는 13개 로케일 × 수백 개 키를 요청마다 조립하는 구조에서, 로케일별 Map을 캐시가 유지하는 구조로 바꿨다.

// 개념 설명용 의사코드
class LanguageCache extends ReloadableKeyedCache<...> {
    private Map<Locale, Map<String, String>> perLocale = new HashMap<>();

    protected List<LangRow> loadFromRepo() {
        List<LangRow> rows = repo.findAll();
        writeLockJob(() -> {
            perLocale.clear();
            for (LangRow r : rows)
                for (Locale loc : Locale.values())
                    perLocale.computeIfAbsent(loc, k -> new HashMap<>())
                             .put(r.getKey(), r.getValue(loc));
        });
        return rows;
    }

    public Map<String, String> get(Locale loc) {
        return readLockJob(() -> perLocale.get(loc));
    }
}

요청 시엔 cache.get(locale) 한 줄. 응답 객체가 캐시 안에서 참조로 공유된다. 요청마다 수백 개 키 × 로케일별 분기를 돌려 HashMap을 새로 만들던 로직이 사라졌고 GC 압력이 확 줄었다.

리로드 시 부분 상태가 노출되지 않도록 ReentrantReadWriteLock으로 일관성을 잡았다. ConcurrentMap만으로는 "clear + 여러 put"의 스냅샷 일관성이 보장되지 않는다 — 캐시 아키텍처의 동시성 섹션에 같은 패턴을 더 자세히 풀어뒀다.

외부 벤더 메시지(Betradar 등)는 데이터 소스와 업데이트 주기가 달라서 별도 캐시로 분리했다. 같은 캐시에 묶으면 한쪽 변경에 다른 쪽까지 불필요하게 리로드된다.


삽질 포인트

외부 라이브러리와의 키 충돌. 특정 키 네임스페이스가 라이브러리 내부 예약어와 겹쳐 번역이 엉뚱하게 뜨는 hotfix를 한 번 쳤다. 근본 수정은 앱 키에 prefix를 박아 공간 자체를 분리한 것. 외부 라이브러리와 번역 키 공간을 공유하면 언제든 터진다.

일본어만 전각 괄호. 마켓 이름 정리 정규식이 (, )를 타겟으로 했는데, 일본어 마켓 이름은 전각 (, )로 들어왔다. 한 정규식으로 다 처리하려다 실패했고 로케일 체크 후 분기했다. "i18n은 문자열 치환이 아니라 로케일별 파싱 규칙"이라는 걸 실감한 지점이다. 숫자 구분자, 날짜 포맷, 괄호 — 로케일마다 다 다르다.


협업

이 시스템은 프론트·백엔드 양쪽을 내가 작업한 드문 케이스였다. 덕분에 번역 키 네임스페이스를 앱 → 번역팀 → DB → 캐시 → derived까지 한 사람이 설계할 수 있었다. 결정적이었던 건 번역팀과의 계약이었다 — "키는 앱이 정의, 값은 번역팀이 운영"이라는 경계를 먼저 세웠고, 위치 파악이 쉬운 네이밍(home.header.title 같은 점 구분 계층)을 정해서 넘겼다. 이 네이밍이 나중에 키 충돌 이슈를 prefix로 풀 때 기반이 됐다.

어드민팀과의 계약은 "어떤 테이블이 바뀌었다"를 한 필드로 전달하는 것. 리스트 UI 변경과 저장 트리거만 어드민이 담당하고, 캐시 내부는 내가 맡았다. 이 계약을 MQ 단계에서 단순히 유지한 덕에 캐시 종류가 늘어나도 어드민은 건드리지 않아도 됐다.

PR 리뷰에서는 치환 파이프라인 의존 그래프를 직접 그려 올렸다. derived가 어떻게 합성되는지 코드만으로는 파악이 어려워서, 화살표 다이어그램으로 "이 derived는 무엇에 의존하는가"를 한 장으로 보여줬다.


지금 보면

Svelte 5의 rune($derived)을 쓰면 합성이 더 깔끔해진다는 건 명확하다. 더 의미 있는 회고는 다른 지점에 있다.

번역 키 관리를 코드에서 완전히 분리하지 못한 것. 당시엔 "키는 앱이 정의"라고 선을 그었지만 결과적으로 코드에 상수 문자열로 키가 박혔다. 번역 키를 타입 시스템으로 뽑아내서 "존재하지 않는 키를 참조하면 컴파일 에러"가 되도록 했다면, 키 누락 버그가 prod에 올라가는 경로 자체가 닫혔을 것이다. 다음에 같은 문제를 풀면 빌드 타임에 키를 검증하는 파이프라인을 먼저 세울 것 같다.

외부 벤더 캐시의 데이터 동기화 전략. 외부 공급사 데이터 업데이트 주기를 깊게 파악하지 않고 "다르니까 분리"까지만 했다. 실제로는 공급사 업데이트 이벤트를 받는 웹훅이나 스케줄 기반 폴링 중 어느 쪽이 맞는지를 운영 중에 자꾸 조정했다. 설계 단계에서 공급사 API 계약을 더 파고들었으면 이 흔들림을 줄일 수 있었다.


관련 문서

  • Ehcache 캐시 설계 — 같은 리로드 캐시 기반 + MQ 전파 구조
task 카테고리의 다른 글 보기수정 제안하기

댓글

댓글을 불러오는 중...
  • 13개 로케일 다국어 시스템 — Svelte derived 합성 + 백엔드 캐시 사전 구성
  • 요구사항이 만든 제약
  • 전체 구조
  • 프론트엔드 — Svelte derived로 반응형 다국어
  • 언어 데이터 store
  • derived를 "함수를 반환하는 store"로 쓴다
  • 템플릿 치환 — derived의 합성
  • 미번역 키 감지
  • 백엔드 — 응답 시점 계산을 캐시 시점으로
  • 삽질 포인트
  • 협업
  • 지금 보면
  • 관련 문서