ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 웹은 실행 중인 화면을 열어 볼 수 있다 — '실제 적용된 값'을 대조하는 세 가지 창구
    IT 2026. 8. 12. 21:00

    코드에는 분명 브랜드 파랑 토큰을 썼는데, 화면의 버튼은 빨갛게 떠 있던 적이 있다. 범인은 다른 파일에 있던 더 구체적인 선택자였다 — CSS의 cascade(여러 스타일 규칙이 충돌할 때 우선순위를 정해 하나를 고르는 규칙) 때문에, 내가 선언한 값과 화면에 실제로 적용된 값이 달라진 것이다. 소스 코드만 검사하는 정적 린트는 이런 문제를 원리적으로 잡을 수 없다. 코드에는 아무 잘못이 없기 때문이다.

    그런데 웹에는 다른 플랫폼에 없는 특권이 하나 있다. 실행 중인 화면의 최종 상태를 표준 API로 열어 볼 수 있다. 문서 객체 모델(DOM — 브라우저가 HTML을 파싱해 만드는, 화면 요소들의 트리 구조)과 계산된 스타일이 런타임에 그대로 노출되므로, "코드에 뭐라고 적었는가"가 아니라 "화면에 실제로 뭐가 적용됐는가"를 기계가 물어볼 수 있다. 이 글은 그 창구의 대표 예시 세 가지를 — 색, 크기, 명도 대비 — 실제 코드로 하나씩 열어 본다.

    선언한 값과 적용된 값 사이에는 개입자가 있다

    왜 "적용된 값"을 따로 물어봐야 할까? 선언과 적용 사이에 여러 단계가 개입하기 때문이다.

    diagram

    다이어그램 설명. "소스 코드"에서 선언한 값이 두 개입 단계 — 더 구체적인 선택자의 덮어쓰기, 그리고 상속·테마 전환 — 를 통과한 뒤에야 "실제 화면 적용값"이 된다. 핵심은 이 최종값이 소스 코드 어디에도 문자 그대로 적혀 있지 않다는 점이다. 개입 단계들의 결과는 브라우저가 렌더링을 하면서 비로소 계산되고, 그 계산 결과를 "런타임 조회 창구"가 그대로 돌려준다. 그래서 검증 단계에서는 소스를 다시 해석할 필요 없이 브라우저가 이미 내린 결론을 받아 대조만 하면 된다 — 소스 분석으로 이 값을 알아내려면 cascade 전체를 재현해야 하는데, 그건 브라우저를 하나 더 만드는 일에 가깝다.

    창구 1 — getComputedStyle: 최종 스타일을 그대로 읽는다

    첫 번째이자 가장 기본이 되는 창구는 getComputedStyle이다. 어느 요소든 넘기면, 브라우저가 cascade와 상속을 모두 끝낸 뒤의 최종 확정값(resolved value)을 돌려준다.

    // 브라우저 콘솔에서 바로 실행해 볼 수 있다
    const btn = document.querySelector('.buy-button');
    const cs = getComputedStyle(btn);
    
    cs.backgroundColor;  // "rgb(220, 38, 38)" ← 소스의 파랑 토큰이 아니라 빨강!
    cs.fontSize;         // "14px"  ← rem으로 선언했어도 px로 환산된 최종값
    cs.display;          // "flex"
    

    코드 설명. 요소 하나를 골라 계산된 스타일을 조회하는 세 줄이다. 눈여겨볼 것은 반환값의 형태다 — 색은 선언을 어떤 방식으로 했든(#DC2626, 토큰 변수, red 키워드) rgb 형태로 통일되어 나오고, 글자 크기도 상대 단위(rem)로 선언했든 최종 px로 환산되어 나온다. 이 정규화가 검증 자동화의 토대다 — 표기법이 제각각인 선언값끼리 비교하는 게 아니라, 브라우저가 통일해 준 최종값끼리 비교하므로 대조가 단순한 문자열 비교가 된다. 도입부의 "파란 토큰을 썼는데 빨간 버튼" 문제는 이 창구로 즉시 드러난다 — 디자인 토큰이 정의한 파랑의 rgb값과 화면의 rgb값을 비교하면 끝이다.

    창구 2 — getBoundingClientRect: 실제 그려진 크기와 위치

    스타일 값만으로는 부족할 때가 있다. "이 버튼이 실제로 몇 픽셀 크기로 그려졌는가"는 CSS 선언만 봐서는 알 수 없다 — 최종 크기는 내용물의 길이, 폰트, 부모 컨테이너 폭이 함께 결정하기 때문이다. 두 번째 창구 getBoundingClientRect는 요소가 화면에 실제로 차지한 사각형(위치와 크기)을 돌려준다.

    // 실전 예시: 터치 타깃 크기 검사
    // WCAG 2.2(웹 접근성 표준 최신판)는 클릭/터치 대상이
    // 최소 24×24 CSS 픽셀이어야 한다고 요구한다 (Target Size 기준)
    document.querySelectorAll('a, button').forEach((el) => {
      const r = el.getBoundingClientRect();  // 실제 렌더된 사각형
      if (r.width < 24 || r.height < 24) {
        console.warn(`${r.width}×${r.height}px — 터치 타깃이 너무 작음:`, el);
      }
    });
    

    코드 설명. 페이지의 모든 링크와 버튼을 돌며 실제 그려진 크기를 재고, 접근성 기준 미달을 찾아내는 검사다. 이 검사가 런타임에서만 가능한 이유를 짚어 보면 — 소스에는 width: 24px 같은 선언이 아예 없는 요소가 대부분이다. 링크의 크기는 링크 텍스트가 몇 글자인지, 폰트가 몇 px인지에 따라 결정되는 값이지 선언되는 값이 아니다. 그려 보기 전에는 존재하지 않는 값이므로, 그려진 결과를 물어보는 것 외에 방법이 없다. 같은 창구로 요소끼리 겹쳤는지(두 사각형의 교집합 계산), 컨테이너 밖으로 잘렸는지도 판정할 수 있다 — 디자인 QA 도구들이 "버튼이 모바일 폭에서 잘립니다"를 자동으로 찾아내는 원리가 이것이다.

    창구 3 — 명도 대비 검사: 접근성 도구가 이 창구 위에 서 있다

    앞의 두 창구를 조합하면 사람이 수동으로 하던 검증을 통째로 자동화할 수 있는데, 그 대표 사례가 명도 대비(색 대비) 검사다. WCAG는 일반 크기 텍스트의 글자색과 배경색이 4.5:1 이상(큰 텍스트는 3:1)의 대비를 갖도록 요구한다. 오픈소스 접근성 검사 엔진 axe-core(Chrome의 Lighthouse 감사에도 내장된 사실상의 표준 엔진)가 이 검사를 하는 방식이 정확히 "런타임 창구 조합"이다.

    // axe-core 명도 대비 검사의 개념 흐름 (실제 구현을 단순화한 의사코드)
    // 1단: 렌더된 화면에서 글자색을 읽는다 — 창구 1
    const fg = getComputedStyle(textEl).color;
    // 2단: 배경색은 한 번에 안 나온다 — 반투명이면 조상 요소를 거슬러
    //      올라가며 실제 보이는 색을 합성해야 한다
    const bg = resolveEffectiveBackground(textEl);  // 조상 배경 + 알파 합성
    // 3단: 두 최종 색으로 대비를 계산해 4.5:1과 비교한다
    const ratio = contrast(fg, bg);
    if (ratio < 4.5) report(textEl, ratio);
    

    코드 설명. 대비 검사가 왜 런타임 검사일 수밖에 없는지가 배경색을 구하는 단계에 담겨 있다. 글자의 배경은 자기 자신이 아니라 뒤에 깔린 조상 요소가 그리는 경우가 많고, 반투명(알파)이 끼면 여러 겹의 색을 합성해야 "사용자 눈에 실제로 보이는 배경색"이 나온다. 소스 어디에도 "이 글자의 최종 대비는 3.2:1"이라는 숫자는 적혀 있지 않다 — 렌더된 화면에서만 계산 가능한 파생값이다. 다크 모드가 있다면 테마마다 이 값이 통째로 달라지므로, 두 테마를 각각 띄워 두 번 재야 한다는 점도 런타임 검사의 성격을 잘 보여준다.

    정직하게 한계도 적어 둔다. axe-core 문서는 배경이 이미지나 그라데이션이거나 다른 요소에 가려진 경우, 자동 판정 대신 "incomplete"(사람 검토 필요)로 분류한다고 명시한다. 런타임 창구가 열어 주는 것은 "계산 가능한 값"까지이고, 사진 위 글자의 가독성처럼 계산으로 환원되지 않는 판단은 여전히 사람 몫으로 남는다.

    창구를 테스트에 박기 — 브라우저 자동화와 CI

    콘솔에서 손으로 실행하는 검사는 한 번의 확인으로 끝난다. 이 창구들의 진짜 가치는 CI(지속 통합 — 코드가 바뀔 때마다 자동으로 도는 검증 파이프라인)에 박을 때 나온다. 브라우저 자동화 도구 Playwright는 위 창구들을 테스트 단언(assertion) 형태로 감싸 제공한다.

    // Playwright 테스트 — 코드가 바뀔 때마다 CI에서 자동 실행된다
    test('구매 버튼은 브랜드 파랑으로 렌더된다', async ({ page }) => {
      await page.goto('/product');
      const btn = page.getByRole('button', { name: '구매' });
    
      // 내부적으로 계산된 스타일(창구 1)을 대조한다
      await expect(btn).toHaveCSS('background-color', 'rgb(37, 99, 235)');
      // 뷰포트 안에 실제로 보이는지(창구 2 계열)도 단언할 수 있다
      await expect(btn).toBeInViewport();
    });
    

    코드 설명. 실제 브라우저로 페이지를 띄우고, 버튼의 계산된 배경색이 브랜드 파랑의 rgb값과 일치하는지 단언하는 테스트다. toHaveCSS는 문서에 명시된 대로 계산된(computed) 스타일을 대조한다 — 즉 도입부의 "다른 선택자가 덮어써서 빨개진 버튼"은 이 테스트를 통과하지 못한다. 소스 코드는 그대로인데 화면이 달라지는 회귀(스타일 파일 추가, 라이브러리 업데이트, 테마 변수 변경)를 코드 리뷰가 아니라 기계가 잡게 되는 것이다. 이런 검증은 사람이 매번 눈으로 확인하기에는 지루하고 빠뜨리기 쉬운, 자동화가 정확히 이기는 종류의 일이다.

    정리 — 도구가 똑똑해서가 아니라, 플랫폼이 창구를 열어줬기 때문이다

    웹에서 디자인·접근성 검증 자동화가 유독 앞서 있는 이유를 한 줄로 줄이면 이렇다. 웹은 "실제로 화면에 적용된 값"을 조회하는 표준 창구를 플랫폼 차원에서 열어 두었고, 검증 도구들은 그 창구 위에 서 있다. 계산된 스타일을 읽는 창구, 그려진 기하를 재는 창구, 그리고 그 둘을 조합해 대비 같은 파생값을 계산하는 도구들 — 전부 브라우저가 내린 최종 결론을 받아 쓰는 구조다. 네이티브 앱 환경에 이만한 표준 창구가 없어서 검사가 소스 정적 분석으로 후퇴한다는 것과 대비해 보면, 이 창구가 얼마나 큰 특권인지 드러난다.

    적용은 오늘 바로 시작할 수 있다. 자기 서비스의 아무 페이지나 열고 콘솔에서 getComputedStyle로 버튼 하나의 색을 물어보는 것 — 선언한 값과 다른 값이 나오는 요소가 하나라도 있다면, 그게 런타임 검증을 CI에 박아야 할 이유다.


    참고한 공개 자료:

    • MDN, Window.getComputedStyle() (resolved value·색 직렬화 규칙): https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle
    • MDN, Element.getBoundingClientRect(): https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
    • Deque University, axe color-contrast 규칙 (4.5:1/3:1 기준·incomplete 처리): https://dequeuniversity.com/rules/axe/4.10/color-contrast
    • Playwright, LocatorAssertions — toHaveCSS·toBeInViewport: https://playwright.dev/docs/api/class-locatorassertions
    • W3C, WCAG 2.2 — Target Size (Minimum): https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html

    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.