System Localization Center

개발자를 위한 코드 기반 다국어 메시지·라벨 운영 제작기
calendar icon

2026년 2월 9일

tag icon

다국어 메시지를 “관리”하고 “개발에서 쉽게 쓰기”까지

실 프로젝트 개발을 진행하면서, 다국어 관련 처리가 예상보다 훨씬 불편했었다.

버튼 라벨, 섹션 타이틀, 안내 문구, 토스트 메시지, 삭제 확인 문구까지—UI 전반에서 텍스트는 계속 늘어나는데, 관리는 전혀되지 않아서 연쇄적인 문제가 발생했다.

  • 화면이 늘어날수록 하드코딩 텍스트가 퍼지고
  • 번역 누락/오타 수정이 개발 일정에 계속 얹히고
  • “문구만 바꾸면 되는데” 배포를 해야 하는 상황이 반복되고
  • 컴포넌트마다 토스트/컨펌 호출 방식이 달라 유지보수가 어렵고

그래서 필자는 “다국어 라벨 및 메시지를 편하게 관리하고, LWC 개발에서도 쉽게 적용할 수 있는 구조”를 목표로 개발을 시작했다.

그 프로젝트가 System Localization Center(이하 SLC)이다.

이 글은 프로젝트 과정에 대해 정리한 글로 사용법 및 구체적인 코드를 확인하고 싶다면 SLC 가이드를 확인하길 바란다.

“내가 편하고 싶어”


1. 목표를 두 축으로 정리: UI + Client API

SLC를 만들기 위해 가장 중요한 부분을 두 가지로 정리했다.

  1. UI(관리 화면)

    • 한 화면에서 생성/수정/삭제를 빠르게 처리
    • Definition과 Value를 동시에 관리(활성/비활성 포함)
    • 검색/필터로 운영이 가능한 수준의 UX
  2. Client API(개발자가 LWC에서 쓰는 방식)

    • LWC에서 메시지를 “편하게” 가져오고 적용할 수 있어야 함
    • 템플릿 변수 치환, 에러 메시지 표준화, 캐싱 포함
    • 토스트/컨펌 호출을 최소한의 코드로 일관되게 처리

이 두 축이 충족되어야, “관리 도구”로 끝나지 않고 실제 개발 생산성을 바꿀 수 있다고 판단했다.


2. Definition / Value (Master-Detail)

UI를 만들기 전에 먼저 데이터 구조를 확립했다. 이유는 간단하다.

"하나의 코드(ResourceCode)에 여러 언어 값이 매달리는 구조"는 변하지 않기 때문이다.

앞서 작성한 ContentDocument와 ContentVersion과 비슷한 구조라고 생각하면 편하다. 단, 이력을 남기는 버전이 아닌, 언어에 따른 버전이라는 점이 다르다.

오브젝트 구조

  • System_Resource_Definition__c (마스터)
    • Type/Category (Picklist)
    • Name (Text)
    • Description (Long Text)
    • IsActive (Checkbox)
    • ResourceCode (Text, Unique, Case Insensitive)
    • Owner (Lookup)
  • System_Resource_Value__c (디테일)
    • Message_Definition (Master-Detail)
    • Language (Picklist)
    • MessageText (Long Text)
    • IsActive (Checkbox)
    • Name (Text)

설계 의도

  • Definition: “메시지의 정체성” (코드/분류/설명/활성화)
  • Value: “언어별 실제 문자열” (언어/텍스트/활성화)

3. UI 구성: 한 화면에서 관리가 끝나는 구조

이후 UI는 위 구조를 그대로 반영했다.

  • 좌측: Definition 리스트
    • 검색(Definition Name/ResourceCode)
    • Type 필터
    • Active 필터
    • 선택한 Definition 강조
  • 우측: Definition 상세 + Values 편집
    • Definition 정보 수정(Description, IsActive)
    • 언어별 Value 생성/수정/활성화
    • Missing Only 토글(번역 누락만 보기)
    • Save 버튼(Dirty 상태 기반)

"화면 구성 예시"

"화면 구성 예시"

UX 품질 포인트

최대한 한 페이지 안에서 Definition과 Value를 모두 생성, 수정, 삭제, 조회를 할 수 있도록 개발하였고, Definition이 마스터 데이터이기 때문에 이에 따른 관계나 수정된 요소가 있는지 없는지 실시간으로 감지하여 사용자에게 혼선을 주지 않도록 하였다.

  • Definition이 Inactive이면 Values 영역 전체 비활성화(편집 불가)
  • Dirty Tracking: 값이 원복되면 Save가 다시 비활성화되도록 “원본 스냅샷 비교” 기반

4. UI에서 만든 레코드로 실제 LWC 라벨/메시지를 동적 적용

이렇게 구상한대로 디자인 및 오브젝트 구조까지 개발이 완료됐다.

이제 실사용이 가능한지 확인하기 위해, 실제로 만든 UI로 레코드를 생성하고 이를 기반으로:

  • 화면 라벨(UI 텍스트)
  • 토스트 메시지
  • Confirm 메시지

동적으로 적용해보았고, 기존 화면과 동일하게 나오는 것을 확인했다.

여기까지는 “레코드 기반 메시지 관리”로도 충분해 보였다. 이제 App화하여 한번에 배포가 가능하도록만 정리하면 되겠다 싶어 정리 후 다른 Org에 배포를 진행해 보았으나 이 단계에서 치명적인 문제가 드러났다.


5. 레코드는 따라가지 않는다

Sandbox에서 Production으로 배포했더니, 레코드 기반으로 만들었던 메시지/라벨이 이관되지 않아 컴포넌트가 텅 비어버리는 현상이 발생했다.

“SLC 자체를 다른 org에 설치하거나 배포해서 즉시 사용 가능한 상태로 만들려면, 기본 메시지/라벨이 레코드여서는 불가능 하다”

이 단계에서 참 순간 막막한 느낌이 들었다.

“이미 만들어놓은 레코드로 동적으로 값을 불러오게 전부 설정해놓았는데 롤백을 해야하나?”

이건 있을 수 없는 일이었다. 기껏 동적으로 언어를 감지해서 번역이 되게 구현을 했는데 이걸 다시 하드 코딩으로 돌리는건 너무 어불성설이었다.

“그럼 어떻게 하지? 레코드성 데이터를 전부 한번 더 생성해야하나?”

이것도 말이 안됐다. Definition만 약 60개에 이에 따른 Values도 120개에 달하는데 이걸 매번 App을 설치할 때마다 복사한다? 너무 비효율적이고 사실상 App이라는 이름이 부끄러울 정도였다.

여기에 더해, App에서 필수적으로 사용하는 라벨이나 메세지들을 사용자가 수정을 하게 되면 App 자체가 망가질 가능성이 있다는 것을 알았다. 예를 들어, App의 이름을 정의한 Value를 이상한 이름으로 사용자가 수정하고 기존의 이름을 까먹는다면 영영 돌아올 수 없는 상황이 되어 버리는 것이다.

“이러다 개판 되는거 아냐?”

이 시점부터 “시스템 기본값”과 “사용자 커스텀”을 분리해야 한다는 필요성이 명확해졌다.


6. 시스템/커스텀 분리 전략: CMDT 도입

“시스템 기본값”으로 현재 App에 필요한 라벨, 메세지 등을 모두 정리하고 이후 사용자가 직접 사용하기 위해 생성하는 것들이 “사용자 커스텀”으로 구분을 지었다.

그래서 처음에는 isSystem 같은 필드로 시스템 레코드를 구분하고 UI에서 편집/삭제를 막는 방식도 고려했지만, 곧 한계를 느꼈다.

  • 앱 설치 직후 시스템 레코드가 잔뜩 보이면 UX가 나빠짐
  • ResourceCode가 Unique여야 하는데, 시스템 레코드가 레코드로 존재하면 사용자의 자유도가 제한됨
  • 무엇보다 “기본 제공 값”이 레코드면 배포/설치 안정성이 떨어짐

문제점을 찾았으니 이에 대한 해결책을 하나씩 생각해보았다.

  • UX가 나빠지니 특정 태그를 붙여서 권한별로 보이고 안보이게 설정을 할까?
  • ResourceCode가 중복이 될 수 있으니 SYS_라는 태그를 기본적으로 붙여서 구분을 지어야겠다.
  • 세일즈포스에서 레코드가 아니면 메타데이터 밖에 없는데?

그래서 위의 모든 조건을 충족시킬 수 있는 방법으로 시스템 기본값은 Custom Metadata Type(CMDT) 으로 분리했다.

시스템 기본 메시지 소스(CMDT)

  • System_Resource_Definition__mdt
  • System_Resource_Value__mdt

사용자 커스텀 소스(Record)

  • System_Resource_Definition__c
  • System_Resource_Value__c

7. 조회 우선순위: Default는 CMDT, Override는 Record

핵심 결정은 이 구조였다.

  • 기본적으로는 CMDT를 조회(“기본 제공”)
  • 사용자가 동일한 ResourceCode로 레코드를 만들면 Record가 우선(“커스터마이징”)

우선은 시스템의 기본값은 항상 존재해야한다고 생각했다. Default라는 값이 괜히 있는게 아니다. 이를 위해 CMDT를 활용해서 아무나 접근하지 못하는 곳(Admin Only)에 Default 값들을 저장해 언제든 사용할 수 있도록 하였다.

더불어 메타데이터 값은 화면 좌측의 Definition 목록에서 보이지 않도록하여 초기 UX를 깔끔하게 하였고 ResourceCode에 SYS_태그를 붙여서 커스텀 레코드와 잘 중복이 되지 않게끔 하였다.

또, 사용자에게 혹시라도 UI의 문구나 번역이 마음에 들지 않는다거나, 기본 값으로 언어가 지원이 안되는 경우에 한해서 설정할 수 있도록 하기 위해서 “Overwrite”기능을 추가하였다. 기본적으로는 CMDT를 조회하되 같은 ResourceCode값으로 Definition 및 Value를 생성하면 이를 통해 기본 제공 값을 덮어씌울 수 있는 기능이다.

즉, Default는 Metadata, Override는 Record 인 셈이다.

“어, 그러면 ResourceCode가 Unique하지 않게 되는거 아닌가?”

아니다. 여전히 레코드 상에서의 Unique는 보장된다. 실제로도 같은 ResourceCode로 Definition을 생성하려고 할 때는 생성되지 않는 모습을 확인할 수 있다.

image.png

장점 정리

  • 앱 설치 즉시 메시지/라벨이 존재 → 화면이 비지 않음
  • 시스템 기본값은 UI에 노출되지 않음 → 초기 UX 깔끔
  • 사용자가 필요 시 같은 ResourceCode로 덮어써서 커스터마이징 가능
  • 배포/이식 안정성 확보(CMDT는 메타데이터로 배포 가능)

8. “덮어쓰기 상태”를 UI에 표시: SYSTEM / SYSTEM OVERWRITING

Overwriting 구조를 도입하면, 사용자 입장에서는 “내가 만든 레코드가 시스템 기본값을 덮는지”를 알 수 있어야 한다.

그래서 다음 표시를 추가했다.

  • CMDT 기반이면: System Resource Registry 탭에서 별도 확인 가능

  • Record 기반인데 ResourceCode가 시스템 코드와 중복이면: OVERWRITE 태그 표시

    "Overwrite 태그 예시" "Overwrite 태그 예시"

표시는 “입력 중 실시간 감지”가 아니라,

  • Definition 리스트 로딩 시점
  • Definition 상세 로딩 시점

에서만 감지하여 표시하도록 했다.


9. 메시지는 정적 문자열이 아니다: 템플릿 + 변수 치환(Interpolation)

UX 문제도 해결했고, 사용자 자유도도 제공해줬고 App 배포도 성공적으로 되게 되면서 다 끝난건가 싶었는데 직장 팀원분이 말씀해 주신 말씀 하나가 또 파문을 일으켰다.

“메세지에 동적 값이 들어가서 나오는건 안돼요?”

실무 메시지는 대부분 값이 들어간다.

  • "{Name}" 저장에 실패했습니다. 사유: {Error}
  • Deleting "{Name}" will also delete {ValueCount} values. Continue?

하.. 정말 끝이 없다..

아무튼 이건 필수 기능이라고 생각했다. 세일즈포스에서도 Template에 여러 변수 값을 받을 수 있게하기도 하고, 메세지에 값이 동적으로 안들어가면 사실상 사용하기 힘들다고도 생각이 들었다.

그래서 메시지는 템플릿 형태(Parameterized Message / Message Template) 로 설계했다.

규칙

기본적인 골자는 세일즈포스에서의 동적 파라미터 사용법과 비슷하다.

  • 템플릿은 {VariableName} 형태의 플레이스홀더를 포함

    image.png "템플릿 예시"

  • 호출 시 vars 객체를 넘기면 해당 키로 치환

    await this.msg.toastSuccess({
        code: SYS_MSG_CODES.CREATE_SUCCESS,
        vars: {
            Name: newName, // <-- Name에 매핑 출력
        },
        fallbackText: "SUCCESS",
    });
    
  • 에러 메시지는 {Error}를 템플릿에 넣으면, 시스템이 자동 주입

    image.png "Error 예시"

    mergeVarsWithError(vars, err) {
            if (!err) return vars || {};
            const errorText = this.normalizeError(err);
            return { ...(vars || {}), Error: errorText };   // <-- Error에 매핑 출력
        }
    

이렇게 하면 운영자는 “문구”만 관리하고, 개발자는 “변수”만 넘기면 되므로 역할이 분리된다.


10. 성능/개발 생산성: 일괄 로딩 + 캐싱 + 표준 래퍼

이후부터는 메시지 호출의 일관성재사용성 최적화 작업을 진행했다.

(1) 고정적으로 필요한 UI 라벨

  • 화면 구성에 계속 필요
  • 매번 단건 호출은 비효율적 → 초기 진입 시 일괄 로딩 + 캐싱

(2) 이벤트 기반 메시지(토스트/컨펌)

  • 특정 액션에서만 필요
  • 하지만 한 번 호출된 코드는 재사용 가능 → 온디맨드 호출 + 캐싱

그리고 반복 코드를 줄이기 위해 호출을 표준화했다.

  • toastSuccess({ code, vars, fallbackText })
  • toastError({ code, err, vars, fallbackText })
  • confirm({ code, vars, fallbackText })

11. 최종 완성: SystemLocalizationClient라는 “사용자용 API”로 정리

결국 사용자(개발자)가 원하는 건 단순하다.

  • 메시지를 사용하기 위해 단순 호출로 해결하고 싶고
  • 캐시를 직접 관리하고 싶지 않고
  • 템플릿 치환/에러 정규화 같은 반복 로직을 쓰고 싶지 않다.

그래서 필자는 SystemMessageClient를 만들고,

생성자(또는 create 메서드)로 한 번 생성한 뒤, 메서드를 호출하는 방식으로 설계했다.

결과적으로 SystemLocalizationClient는 “라이브러리”이자

LWC 개발에서 다국어 메시지를 쓰기 위한 사실상의 내부 API 역할이다.


마무리

System Localization Center는 “다국어 문구 관리 UI”로 시작했지만, 실제로는 LWC 개발에서 메시지를 쓰는 방식 자체를 표준화하는 방향으로 확장하였다.

  • 시스템 기본값은 CMDT로 제공(배포 안정성)
  • 사용자는 Record로 덮어쓰기 가능(자유도)
  • UI는 한 화면에서 관리(운영 편의)
  • 개발자는 Client API로 단순 호출(생산성)

“실 프로젝트에서 다국어 처리가 불편해서 시작했지만,

최종적으로는 메시지를 ‘관리 가능’하고 ‘개발에서 쉽게 쓰는 구조’로 만드는 것.”

불편한 부분을 하나씩 개선해나가다보니 어느새 한 달짜리 개인 프로젝트가 되어버렸지만, 개인적으로는 매우 만족스러운 것 같다. 정말 생각할 수 있는 범주 내에서 타협은 일절하지 않고 오로지 내가 실제로 이걸 사용한다면 하는 관점에서 어떻게든 편하고 최대한 간단하게 사용할 수 있도록 만들려고 노력했다.

어떻게 보면 개인적인 욕심이 듬뿍 들어가서 비대해져버린 프로젝트라고 볼 수도 있겠다.

이걸 실제로 활용할 수 있을지는 한번 실제로 사용을 해보고 기회가 된다면 후기를 적어보도록 하겠다.

ps. 블로그 글을 쓰는데 있어서 말투를 어떻게할지 많은 고민을 했지만 결국 이 블로그는 필자의 일기나 다름이 없고, 개인적은 느낀점을 쓰듯이 쓰는게 후에 필자가 읽었을 때 더 와닿을 뿐더러 개인적으로 글을 쓰는데 더 나름의 정성이 들어가는 느낌이라 이 포스트와 같은 말투를 유지하도록 결정했으니 혹시 읽는 사람이 있다면 너른 양해를 바랍니다.


hamburger icon

무링의 개발 블로그

Salesforce Developer


연관된 글