게임 다크모드/라이트모드 UI 디자인 시스템 구축
테마를 색상 교체 기능이 아니라 디자인 토큰과 상태 규칙의 문제로 다루는 방법을 정리합니다. Unity와 Unreal 등 엔진 UI에 적용할 수 있는 구조와 검증 기준을 함께 설명합니다.

테마 전환을 색상 교체로 끝내면 생기는 문제
다크모드와 라이트모드는 배경색만 반전하는 기능이 아니다. 같은 버튼이라도 기본, 비활성, 호버, 눌림, 선택 상태에서 충분한 대비를 유지해야 한다. 경고와 성공처럼 의미를 전달하는 색도 테마에 맞춰 밝기와 채도를 조정해야 한다.
색상 값을 위젯마다 직접 적어 두면 변경 범위가 빠르게 커진다. 새 화면을 만들 때마다 기존 규칙을 복사하게 되고 일부 팝업만 어두운 테마에서 읽기 어려운 문제가 생긴다. 해결의 핵심은 화면이 색상 자체를 알지 않게 하고 역할을 가진 토큰만 참조하게 만드는 것이다.
먼저 역할 기반 토큰을 정의한다
#202124처럼 실제 색상 코드를 바로 쓰는 대신 Surface.Base, Text.Primary, Action.Primary처럼 UI에서 맡은 역할을 이름으로 표현한다. 토큰 이름은 색의 외형보다 용도를 설명해야 한다. Blue500은 특정 색을 강제하지만 Action.Primary는 테마와 브랜드 변경에도 유지할 수 있다.
다음은 게임 메뉴 UI에 쓸 수 있는 최소 토큰 집합이다.
| 영역 | 토큰 예시 | 용도 |
|---|---|---|
| 배경 | Surface.Base, Surface.Raised, Surface.Overlay | 화면, 패널, 모달의 바탕 |
| 텍스트 | Text.Primary, Text.Secondary, Text.Disabled | 제목, 설명, 비활성 문구 |
| 상호작용 | Action.Primary, Action.PrimaryPressed, Action.Danger | 버튼과 파괴적 동작 |
| 경계 | Border.Subtle, Border.Strong, Focus.Ring | 구획, 선택 표시, 키보드 포커스 |
| 상태 | State.Success, State.Warning, State.Error | 결과와 피드백 |
토큰은 지나치게 세분화하지 않는 편이 좋다. 예를 들어 Text.InventoryItemName 같은 토큰은 특정 화면에 묶인다. 먼저 공통 역할 토큰으로 해결하고 여러 화면에서 반복되는 특별한 의미가 있을 때만 별도 토큰을 추가한다.

토큰과 컴포넌트를 분리하는 구조
컴포넌트는 토큰을 참조하고 현재 테마가 토큰의 실제 값을 결정하도록 구성한다. 이 구조라면 버튼 구현은 두 테마의 색상 값을 몰라도 된다.
flowchart LR
A[UI 컴포넌트] --> B[의미 토큰]
B --> C{활성 테마}
C --> D[라이트 팔레트]
C --> E[다크 팔레트]
D --> F[렌더링 색상]
E --> F
이 분리는 게임 엔진과도 잘 맞는다. Unity에서는 ScriptableObject로 토큰 테마 에셋을 만들고 Unreal에서는 Data Asset이나 Data Table로 같은 개념을 표현할 수 있다. 중요한 점은 엔진별 저장 방식이 아니라 위젯이 의미 토큰을 통해 값을 받는다는 계약이다.
Unity용 데이터 모델 예시
아래 예시는 토큰을 문자열로 검색하는 단순한 형태다. 규모가 커지면 문자열 대신 열거형이나 생성 코드로 오타를 줄이는 편이 좋다.
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class ColorToken
{
public string id;
public Color value;
}
[CreateAssetMenu(menuName = "UI/Color Theme")]
public class ColorTheme : ScriptableObject
{
[SerializeField] private List<ColorToken> colors;
public Color Get(string id)
{
var token = colors.Find(item => item.id == id);
if (token == null)
{
throw new KeyNotFoundException($"Color token not found: {id}");
}
return token.value;
}
}
버튼은 Action.Primary와 Action.PrimaryPressed를 받고 테마 변경 이벤트가 발생하면 자신이 참조하는 색만 다시 적용한다. 화면 전체를 재생성하지 않아도 되므로 설정 메뉴에서 즉시 테마를 바꾸는 동작에도 적합하다.
대비와 상태를 수치로 검토한다
텍스트와 배경의 명도 대비는 감각만으로 판단하기 어렵다. 상대 휘도 를 사용하면 두 색의 대비비는 다음처럼 계산한다.
일반적인 작은 텍스트는 높은 대비가 필요하다. 반면 장식 요소나 큰 제목은 같은 기준을 그대로 적용할 필요가 없을 수 있다. 다만 숫자만 충족했다고 읽기 좋은 UI가 되는 것은 아니다. 폰트 굵기, 글자 크기, 외곽선, 배경 이미지의 복잡도도 함께 확인해야 한다.
특히 다음 항목은 테마별로 별도 점검한다.
- 비활성 버튼이 일반 텍스트보다 더 눈에 띄지 않는지
- 경고 색이 배경과 섞여 상태 의미를 잃지 않는지
- 포커스 링이 마우스와 게임패드 조작에서 모두 분명한지
- 반투명 모달과 블러 배경 위의 텍스트가 읽히는지
- 아이콘이 색상만으로 활성 여부를 전달하지 않는지
색각 다양성을 고려하면 성공은 체크 아이콘, 오류는 경고 아이콘처럼 형태와 문구를 함께 제공하는 것이 안전하다. 빨강과 초록만으로 성공과 실패를 구분하면 일부 사용자에게 상태가 모호해질 수 있다.
이미지와 셰이더도 테마의 일부로 다룬다
UI 아이콘을 단순히 흰색으로 고정하면 라이트모드에서 경계가 사라질 수 있다. 아이콘에는 Icon.Primary, Icon.Secondary, Icon.OnAction 같은 토큰을 적용한다. 가능하다면 단색 마스크 아이콘을 사용해 테마 색을 입히고 일러스트처럼 원본 색이 중요한 자산은 테마 전용 버전을 준비한다.
배경이 있는 HUD나 지도 UI는 더욱 주의해야 한다. 테마 전환 뒤에도 게임 월드와 UI가 충돌하지 않는지 확인해야 한다. 예를 들어 눈 덮인 맵에서는 라이트모드 패널의 그림자만으로 경계를 만들기 어렵다. 이때는 불투명도, 테두리, 배경 블러 중 한 가지 이상을 함께 사용해 패널의 윤곽을 보장한다.
전환 로직과 사용자 설정
테마는 시스템 설정을 기본값으로 삼되 게임 안에서 사용자가 명시적으로 선택할 수 있게 하는 방식이 일반적이다. 사용자가 선택한 값은 저장 데이터에 기록하고 없다면 운영체제 설정을 읽어 초기 테마를 정한다.
public enum ThemePreference
{
System,
Light,
Dark
}
public static class ThemeResolver
{
public static bool UseDarkTheme(ThemePreference preference, bool systemUsesDark)
{
switch (preference)
{
case ThemePreference.Dark:
return true;
case ThemePreference.Light:
return false;
default:
return systemUsesDark;
}
}
}
전환 애니메이션은 짧고 예측 가능해야 한다. 색상을 부드럽게 보간할 수는 있지만 전환 중 텍스트 대비가 잠시 낮아지는 조합은 피해야 한다. 접근성 설정에서 동작 감소를 선택한 경우에는 즉시 전환하거나 애니메이션 시간을 크게 줄이는 편이 낫다.
출시 전 검증 체크리스트
테마별 스크린샷을 주요 해상도와 언어 조합으로 자동 캡처하면 회귀를 줄일 수 있다. 긴 번역문, 숫자가 많은 상점 가격, 잠금 아이콘, 알림 배지처럼 공간과 대비에 민감한 요소를 포함해야 한다.
- 시작 화면, HUD, 인벤토리, 상점, 설정, 모달을 모두 확인한다.
- 기본·호버·눌림·비활성·선택·포커스 상태를 캡처한다.
- 밝은 월드와 어두운 월드 장면 위에서 HUD 가독성을 확인한다.
- 테마를 변경한 뒤 새로 열린 팝업에도 올바른 테마가 적용되는지 확인한다.
- 토큰에 없는 임의 색상 값이 UI 코드와 에셋에 남아 있는지 검색한다.
다크모드와 라이트모드의 품질은 팔레트의 개수보다 규칙의 일관성에서 나온다. 역할 기반 토큰, 상태별 대비 기준, 엔진에 독립적인 테마 적용 구조를 먼저 만들면 새 기능과 새 화면이 추가되어도 테마 지원 비용을 안정적으로 유지할 수 있다.



