06 — RTL & Localization
이 문서가 답하는 질문: 아랍어·히브리어 같은 RTL 언어 지원을 디자인 시스템 레벨에서 어떻게 처리해야 하나?
margin-left를 모두margin-right로 바꾸는 RTL 빌드를 따로 만들어야 하나? 그리고 한자·아랍어처럼 언어별 폰트 스택은 토큰으로 어떻게 다루는가? 한 줄 답 (Pyramid Top): 정답은 “CSS의 logical properties(margin-inline-start,padding-block-end)를 처음부터 쓰고, locale-specific 자산(font stack, line-height)만 토큰으로 분기” —dir="rtl"속성 하나가 브라우저에게 “물리적 좌우를 뒤집어라”고 신호하면 logical property를 쓴 모든 코드가 자동으로 미러된다. RTL 빌드를 따로 만들지 마라.
Why — 왜 존재하는가
2010년대 RTL 지원의 표준 해법은 CSS 빌드 두 벌 — Yahoo CSSO나 RTLCSS 같은 도구가 모든 margin-left를 margin-right로 swap해서 app.rtl.css를 생성. 문제:
- 번들 두 벌, 캐시 두 벌.
- 조건부 좌우(예: 화살표는 미러, 로고는 그대로)를 제어 못 함.
- 새 페이지마다 두 번 검수.
CSS Logical Properties(2018 사양, 2020년경 안정화)가 이 문제를 언어 차원에서 풀었다. margin-left 대신 margin-inline-start를 쓰면 LTR에서는 왼쪽 = inline-start, RTL에서는 오른쪽 = inline-start. 한 CSS, 두 방향 모두 작동.
| 풀려는 문제 | 이전 해법 | 한계 |
|---|---|---|
| 좌우 미러링 | RTLCSS로 swap 빌드 | 번들 2배, 조건부 제어 X |
dir="rtl" 컴포넌트 분기 | [dir="rtl"] .foo { ... } 수동 작성 | 모든 컴포넌트에 분기 |
| 화살표·아이콘 미러 | 별도 SVG 두 벌 | 자산 2배 |
| 한자·아랍어 폰트 | 모든 컴포넌트에서 폰트 분기 | drift |
logical properties + 토큰 시스템 측 언어별 폰트 스택이 2025년 사실상 표준이다.
How — 어떻게 동작하는가
logical property + dir 속성의 결합이 핵심. CSS는 모르고도 자동 미러.
What — 구체 사양 / 수치 / 예시
Physical vs Logical 매핑
| 물리적 (피해라) | 논리적 (써라) | LTR 의미 | RTL 의미 |
|---|---|---|---|
margin-left | margin-inline-start | 왼쪽 | 오른쪽 |
margin-right | margin-inline-end | 오른쪽 | 왼쪽 |
padding-top | padding-block-start | 위 | 위 (수직은 같음) |
padding-bottom | padding-block-end | 아래 | 아래 |
border-left | border-inline-start | 왼쪽 | 오른쪽 |
left: 0 | inset-inline-start: 0 | 왼쪽 끝 | 오른쪽 끝 |
text-align: left | text-align: start | 왼쪽 정렬 | 오른쪽 정렬 |
width | inline-size | 너비 | 너비 |
height | block-size | 높이 | 높이 |
수직 축(block) 은 보통 RTL에서도 안 바뀐다 — 수직 텍스트(일본어 세로쓰기) 같은 writing-mode까지 가야 block 축이 변한다.
Tailwind v3의 logical utilities
Tailwind v3.3+ 기본 제공:
| Tailwind 유틸 | 의미 |
|---|---|
ms-4 | margin-inline-start: 1rem |
me-4 | margin-inline-end: 1rem |
ps-2 | padding-inline-start: 0.5rem |
pe-2 | padding-inline-end: 0.5rem |
start-0 | inset-inline-start: 0 |
text-start | text-align: start |
{/* 잘못된 예 (RTL에서 안 미러됨) */}
<div class="ml-4 pr-2 text-left">...</div>
{/* 올바른 예 */}
<div class="ms-4 pe-2 text-start">...</div>Panda CSS
Panda는 별도 변환 없이 CSS native logical property를 그대로 사용:
import { css } from 'styled-system/css';
<div className={css({
marginInlineStart: '4',
paddingInlineEnd: '2',
textAlign: 'start',
})}>또는 축약 alias:
// panda.config.ts
import { defineConfig } from '@pandacss/dev';
export default defineConfig({
utilities: {
ms: { className: 'ms', values: 'spacing', transform: (v) => ({ marginInlineStart: v }) },
me: { className: 'me', values: 'spacing', transform: (v) => ({ marginInlineEnd: v }) },
},
});dir 속성 — 어디에 박나
{/* 페이지 전체 RTL */}
<html lang="ar" dir="rtl">
...
</html>
{/* 페이지 LTR이지만 특정 영역만 RTL */}
<html lang="ko" dir="ltr">
<body>
<p>한국어 단락 안의 <span dir="rtl">العربية</span> 인용</p>
</body>
</html>
dir="auto"는 내용에 따라 자동 결정 — 사용자 입력 필드에 유용 (<input dir="auto">).
Locale-specific font stack — 토큰으로
언어별로 최적 본문 폰트가 다르다. 한자(CJK)는 시스템 sans보다 Noto Sans CJK가 가독성 좋고, 아랍어는 Noto Sans Arabic. 영문 폰트는 한자/아랍자 글리프가 없거나 못생긴 fallback.
토큰화 패턴:
// panda.config.ts
conditions: {
ko: '[lang^=ko] &',
ja: '[lang^=ja] &',
zh: '[lang^=zh] &',
ar: '[lang^=ar] &',
},
theme: {
semanticTokens: {
fonts: {
'body': {
value: {
// 기본 (영문 우선, 그 다음 universal fallback)
base: 'Inter, system-ui, -apple-system, sans-serif',
_ko: 'Inter, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif',
_ja: 'Inter, "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
_zh: 'Inter, "PingFang SC", "Noto Sans SC", sans-serif',
_ar: '"Noto Sans Arabic", "Geeza Pro", sans-serif',
},
},
},
},
},{/* 한국어 페이지 */}
<html lang="ko">
{/* 자동으로 _ko 분기 — Apple SD Gothic Neo 적용 */}
</html>Locale-specific line-height
한자·아랍어는 보통 영문보다 큰 line-height가 필요하다 (글리프가 세로로 더 큼).
semanticTokens: {
lineHeights: {
'body': {
value: {
base: '1.5', // 영문 기본
_ko: '1.6', // 한글
_ja: '1.7', // 일본어
_zh: '1.7',
_ar: '1.8', // 아랍어 — diacritics 공간
},
},
},
}미러되지 말아야 할 자산
| 자산 | RTL에서 미러? | 이유 |
|---|---|---|
| 텍스트 정렬, 패딩, margin | YES | 흐름 그대로 |
| 화살표(prev/next, back) | YES | ”다음”이 RTL에서는 왼쪽 |
| 로고 | NO | 브랜드 그대로 |
| 시계 아이콘, 시간 표시 | NO | 시간 흐름은 만국 공통 |
| 비디오 progress bar | NO (혹은 미러) | 미디어 컨트롤은 신중 |
| 체크박스 ✓ | NO | 보편 기호 |
| 코드, URL, 이메일 (LTR 텍스트) | NO | <span dir="ltr"> 명시 |
미러 제어:
/* 화살표 — 자동 미러 */
.icon-arrow-next {
/* SVG 자체는 LTR 방향 */
}
[dir="rtl"] .icon-arrow-next {
transform: scaleX(-1);
}
/* 로고 — 미러 절대 금지 */
.logo {
/* RTL에서도 그대로 — 아무 것도 안 함 */
}양방향 텍스트(Bidi) 처리
{/* 영문 안에 아랍어 — 자동 isolate */}
<p>The product is called <span dir="auto">منتج</span> in Arabic.</p>
{/* 아랍어 안에 영문/숫자 */}
<p dir="rtl">السعر: <span dir="ltr">$29.99</span></p>CSS의 unicode-bidi: isolate도 같은 효과 — 인라인 영역의 방향성을 독립시킨다.
Next.js i18n + RTL
// app/[locale]/layout.tsx
const RTL_LOCALES = ['ar', 'he', 'fa', 'ur'];
export default function LocaleLayout({
children,
params: { locale },
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const dir = RTL_LOCALES.includes(locale) ? 'rtl' : 'ltr';
return (
<html lang={locale} dir={dir}>
<body>{children}</body>
</html>
);
}What-if — 잘못 쓰면 어떻게 깨지는가
- 함정 1 —
margin-left를 남용: RTL에서 모두 미러되지 않음. 대대적 리팩토링 필요. 시작부터 logical property로. - 함정 2 —
text-align: left사용: RTL에서도 왼쪽 정렬 → 어색.text-align: start사용. - 함정 3 — RTLCSS 빌드 + logical property 동시 사용: 빌드 도구가
margin-inline-start를 인식 못 하고 물리적 속성으로 잘못 변환하거나 건너뜀. 둘 중 하나만. - 함정 4 — 폰트 fallback 누락:
font.family.body = "Inter"만. 한자가 시스템 fallback으로 떨어져 가독성 저하. 명시적으로 모든 locale의 폰트 스택을 토큰에 박아라. - 함정 5 — 화살표 미러 누락: “next” 버튼이 RTL에서도 오른쪽 화살표 → 사용자가 혼란. SVG에
[dir="rtl"]selector로transform: scaleX(-1). - 함정 6 —
<input dir>누락: 사용자가 아랍어를 입력하는데dir="ltr"로 강제됨 → 텍스트가 뒤집힘.dir="auto"권장. - 함정 7 —
inline-size대신width사용:writing-mode: vertical-rl(일본어 세로쓰기) 같은 modern layout에서 깨짐. logical size도 처음부터 고려. - 함정 8 — flexbox
flex-direction: row직접 사용: RTL에서는row가 자동으로 오른쪽→왼쪽이 된다 — 이건 정상. 하지만row-reverse를 직접 박으면 RTL에서 원래 의도와 반대가 된다.
Insight — 흥미로운 이야기
CSS Logical Properties는 일본 세로쓰기 요구사항에서 출발했다.
W3C의 CSS Writing Modes 사양은 1999년 일본 출판 업계의 요구로 시작됐다 — 신문·소설의 세로 쓰기를 웹에서 표현하려면 width/height로는 부족했다. 세로쓰기에서는 “줄(line)이 세로로 흐르고 단(column)이 가로로 흐른다”. 즉, width가 “줄의 길이”라는 의미와 분리되어야 한다. 거기서 inline-size(줄 방향 크기) block-size(단 방향 크기)가 나왔다.
logical properties는 RTL을 위해 만든 것이 아니다 — writing-mode를 추상화하다 보니 RTL이 공짜로 따라왔다. CSS의 깊은 추상이 동시에 두 문제(RTL + 세로쓰기)를 해결한 우아한 예.
흥미로운 디테일: **Material Design Web Components(MWC)**는 2018년 공식적으로 모든 CSS를 logical property로 다시 작성했다. 그 결과 RTLCSS 같은 빌드 도구 없이도 RTL이 자동으로 작동하게 됐다. 이게 디자인 시스템 차원의 RTL 지원의 출발점.
또 하나의 반전: 아랍어와 히브리어가 RTL이지만, 숫자는 LTR. “123”을 아랍어 단락 안에서 그대로 적으면 오른쪽에서 왼쪽으로 읽혀 “321”로 보일 수도 있다 — 아니다. 유니코드 BiDi 알고리즘이 숫자 그룹은 LTR로 분리한다. 즉, 아랍어 텍스트에서도 123은 왼쪽 → 오른쪽으로 읽힌다. 디자인 시스템이 이걸 모르고 숫자를 RTL로 강제하면 데이터가 잘못 표시된다.
마지막으로 — GitHub은 2022년에 한국어 페이지에서 “Apple SD Gothic Neo” fallback 누락 버그를 겪었다. macOS 사용자는 잘 보였는데 Windows 사용자에게는 “Malgun Gothic” 시스템 폰트가 적용되어 간격이 어그러졌다. 토큰에 OS별 폰트까지 명시하지 않으면 발생하는 함정. 한국어 디자인 시스템은 보통 'Apple SD Gothic Neo', 'Malgun Gothic', '맑은 고딕', sans-serif 4-단계 fallback이 표준.
요약
- CSS Logical Properties(
margin-inline-start,text-align: start)를 처음부터 써라 — RTL이 자동으로 풀린다. dir="rtl"을<html>에 박으면 logical property를 쓴 모든 코드가 미러된다 — 빌드 두 벌 필요 없음.- Locale-specific font stack을 토큰으로 분기 —
_ko,_ja,_ar등 condition. - 미러 제외 대상(로고, 시계, 코드, 숫자)은
dir="ltr"또는transform: scaleX(-1)로 명시. - BiDi 알고리즘이 숫자는 LTR로 자동 분리 — 디자인 시스템이 이를 깨면 안 된다.