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-leftmargin-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-leftmargin-inline-start왼쪽오른쪽
margin-rightmargin-inline-end오른쪽왼쪽
padding-toppadding-block-start위 (수직은 같음)
padding-bottompadding-block-end아래아래
border-leftborder-inline-start왼쪽오른쪽
left: 0inset-inline-start: 0왼쪽 끝오른쪽 끝
text-align: lefttext-align: start왼쪽 정렬오른쪽 정렬
widthinline-size너비너비
heightblock-size높이높이

수직 축(block) 은 보통 RTL에서도 안 바뀐다 — 수직 텍스트(일본어 세로쓰기) 같은 writing-mode까지 가야 block 축이 변한다.

Tailwind v3의 logical utilities

Tailwind v3.3+ 기본 제공:

Tailwind 유틸의미
ms-4margin-inline-start: 1rem
me-4margin-inline-end: 1rem
ps-2padding-inline-start: 0.5rem
pe-2padding-inline-end: 0.5rem
start-0inset-inline-start: 0
text-starttext-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에서 미러?이유
텍스트 정렬, 패딩, marginYES흐름 그대로
화살표(prev/next, back)YES”다음”이 RTL에서는 왼쪽
로고NO브랜드 그대로
시계 아이콘, 시간 표시NO시간 흐름은 만국 공통
비디오 progress barNO (혹은 미러)미디어 컨트롤은 신중
체크박스 ✓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로 자동 분리 — 디자인 시스템이 이를 깨면 안 된다.