프론트엔드

스크린샷 색상은 sRGB가 아니며, UI에 그대로 표시됩니다

macOS 스크린샷에서 추출한 16진수 값은 sRGB가 아닌 디스플레이 프로필에 있습니다. 이 값을 코드에 붙여넣으면 색상이 달라집니다. 다음은 그 왕복 증명입니다.

이 글은 영어 원문을 AI 모델이 번역한 것입니다. 표현이 원문과 다를 수 있습니다. 영어 원문 보기

스포이트로 스크린샷에서 색상을 샘플링하고 해당 헥스(hex) 코드를 스타일시트에 붙여넣어 색상을 맞추면, 결과가 눈에 띄게 다를 수 있습니다. 그 숫자는 틀리지 않았습니다. 잘못된 공간에서 측정된 것입니다. 넓은 색역(wide-gamut) 디스플레이에서 스크린샷은 디스플레이 프로필로 인코딩되는 반면, 여러분의 CSS, 테마 매니페스트, 디자인 토큰은 sRGB로 해석됩니다. 숫자는 같지만 색상은 다른 것입니다.

저는 이미 올바르게 보이는 한 UI 요소를 다른 요소에 맞추다가 이 문제를 겪었습니다. 제가 샘플링한 값은 #6DCEBD였습니다. 실제로 해당 픽셀을 생성하는 값은 #28D0BC입니다. 이 두 값은 전혀 비슷하지 않습니다.

스포이트가 실제로 제공하는 것

넓은 색역 디스플레이에서 찍은 스크린샷은 sRGB 값의 덤프가 아닙니다. 컴포지터는 디스플레이의 색 공간에 렌더링하며, 스크린샷 파일은 이를 설명하는 ICC 프로필을 포함합니다. 해당 파일을 열고 픽셀을 읽으면 해당 프로필로 표현된 숫자를 얻게 됩니다.

코드는 다르게 읽힙니다. CSS 16진수, 테마 매니페스트 및 대부분의 디자인 토큰 형식은 정의상 sRGB입니다. 파이프라인의 어떤 것도 사용자를 대신하여 변환하지 않습니다. 따라서 왕복 과정에는 두 번의 변환이 있으며, 그중 하나만 사용자에게 보입니다.

샘플링된 16진수가 왕복되지 않는 이유 1 렌더링 2 샘플링 3 다시 붙여넣기 sRGB 소스 40,208,188 화면 표시 109,206,189 샘플링된 16진수 6DCEBD 재렌더링 138,204,190 // 2단계는 프로파일링된 숫자를 읽고, 3단계는 이를 sRGB로 처리합니다

왕복 과정을 통한 증명

이론을 맹신하지 마십시오. 몇 줄만으로 전체 파이프라인을 검증할 수 있습니다. 왜냐하면 여러분은 이미 몇 가지 실측 정보(ground truth)를 알고 있기 때문입니다. 여러분의 코드에서 선언한 모든 색상은 정의상 sRGB 값입니다. 스크린샷에서 해당 색상을 찾아 다시 변환해 보십시오.

from PIL import Image, ImageCms
import io

im = Image.open("screenshot.png")
profile = ImageCms.ImageCmsProfile(io.BytesIO(im.info["icc_profile"]))
to_srgb = ImageCms.buildTransformFromOpenProfiles(
    profile, ImageCms.createProfile("sRGB"), "RGB", "RGB", renderingIntent=0
)
converted = ImageCms.applyTransform(im.convert("RGB"), to_srgb)

선언된 세 가지 색상을 원시(raw)로 샘플링한 후 변환한 결과입니다:

선언된 값 (sRGB) 파일의 원시 픽셀 변환 후
10, 8, 23 10, 8, 22 10, 8, 23
25, 24, 36 25, 24, 35 25, 24, 36
163, 18, 98 147, 33, 96 164, 18, 99

모든 선언된 값이 1 단위 이내로 돌아옵니다. 파이프라인이 확인되었습니다. 이제 일치시키려는 색상에 대해 순방향으로 실행합니다:

sRGB  40, 208, 188   becomes on screen   109, 206, 189
sRGB 109, 206, 189   becomes on screen   138, 204, 190

첫 번째 줄이 정답입니다. 두 번째 줄은 샘플링된 16진수 값을 코드에 바로 붙여넣었을 때의 결과로, 의도한 것처럼 보일 만큼 가깝고 맞추려던 요소 옆에서는 칙칙하게 보일 만큼 틀린 세 번째 색상을 얻게 됩니다.

어두운 색상이 문제를 숨기는 이유

표를 다시 보십시오. 처음 두 행은 단일 단위만큼 이동합니다. 세 번째 행은 빨간색에서 16, 녹색에서 15만큼 이동합니다. 그 비대칭성 때문에 이 버그가 대충 확인해서는 발견되지 않는 것입니다.

색 공간 간의 변환은 무채색 축과 검은색 근처에서 항등 변환에 가깝습니다. 어두운 배경, 테두리, 그림자는 모두 거의 완벽하게 왕복 변환됩니다. 스크린샷을 배경색과 비교하여 일치하는 것을 확인하면, 스크린샷이 sRGB라고 결론 내리고 넘어갑니다.

채도가 있는 곳에 차이가 존재합니다. 강조 색상, 브랜드 색조, 상태 표시자, 구문 강조는 바로 가장 많이 변하는 값이며, 바로 눈으로 맞추려고 할 가능성이 가장 큰 값입니다. 수행한 검사가 통과된 이유는 버그가 존재하지 않는 색역 부분에서 검사를 실행했기 때문입니다.

같은 모양의 두 번째 함정이 있습니다. 도구가 저장할 때 ICC 프로필을 제거하거나 무시하면, 숫자는 그대로 유지되지만 그 의미는 사라집니다. 채팅 앱을 통해 붙여넣거나, 크기를 조절하거나, 다시 인코딩한 스크린샷은 프로필이 전혀 없는 상태로 도착할 수 있습니다. 그 시점에는 파일의 어떤 것도 어떤 공간에 있는지 알려주지 않으며, 유일하게 정직한 대답은 파일만으로는 알 수 없다는 것입니다.

두 가지 해결 방법

명시적으로 변환하기. 절댓값이 필요하다면, 위에서 보여준 변환을 실행하고 변환된 숫자를 사용하세요. 이는 스타일시트, 매니페스트 또는 다른 사람이 읽을 토큰 파일에 색상을 작성해야 할 때 올바른 선택입니다. 나중에 파생 과정을 반복할 수 있도록 원본 스크린샷과 프로필을 그대로 유지하세요.

또는 한 이미지 안에서 비교하기. 실제 목표가 "요소 A를 요소 B와 같은 색으로 만들기"라면, 절댓값은 전혀 필요하지 않습니다. 두 요소를 모두 포함하는 스크린샷을 하나 찍고, 동일한 도구와 동일한 코드 경로로 두 픽셀을 모두 추출한 다음, 서로 비교하세요. 파일이 어떤 색 공간에 있든, 두 샘플 모두 그 안에 있습니다. sRGB 값을 전혀 몰라도 비교는 유효합니다.

두 번째 접근 방식이 더 견고하며, 이제는 이 방법을 먼저 사용합니다. 이 문제를 시작하게 한 사례에서, 최종 확인은 바로 이것이었습니다: 하나의 캡처에서 참조 요소와 대상 요소를 샘플링하고, 색상과 채도를 비교하는 것이었습니다. 색상은 0.0도, 채도는 0.003만큼 차이가 났습니다. 일치하는지 확인하는 데 변환은 필요하지 않았습니다.

두 가지 세부 사항이 이미지 내 비교를 신뢰할 수 있게 만듭니다.

  • 가장자리가 아닌 글리프 내부를 샘플링하세요. 앤티에일리어싱된 텍스트는 경계에서 배경색 쪽으로 혼합됩니다. 평균을 내기보다는 해당 영역에서 가장 채도가 높은 픽셀을 취하세요. 그렇지 않으면 색상 대신 혼합된 색을 측정하게 됩니다.
  • 비활성화된 상태를 주의하세요. 툴바의 회색으로 비활성화된 아이콘은 활성 색상과 거리가 멀어도 채도 필터를 통과할 수 있습니다. 전체 영역을 스캔하고 가장 흔한 값을 신뢰하는 대신, 참조 요소를 신중하게 선택하세요.

이 현상이 나타나는 곳

이 패턴은 특정 플랫폼이나 파일 형식에 국한되지 않습니다. 렌더링된 이미지가 나중에 고정된 공간에서 해석될 값의 원본(source of truth)으로 취급되는 모든 곳에서 나타납니다.

  • 넓은 색 영역(wide-gamut) 기기에서 내보낸 디자인 목업의 색상에 UI 요소를 맞추는 경우.
  • 직접 읽을 수 없는 기존 테마나 스킨 파일을 샘플링하여 빌드하는 경우.
  • 캡처된 픽셀을 하드코딩된 예상 값과 비교하는 시각적 회귀 테스트를 작성하는 경우.
  • CSS에서 사용하기 위해 사진이나 비디오 프레임에서 팔레트를 추출하는 경우.
  • 스크린샷으로 색상 버그를 보고할 때, 스크린샷을 본 사람이 이미지를 샘플링하여 양쪽 모두가 렌더링하지 않은 값을 얻게 되는 경우.

시각적 회귀 테스트는 특별한 주의가 필요합니다. 노트북에서 캡처하고 sRGB 상수에 대해 검증하는 테스트 스위트는 어느 기기에서 실행했는지에 따라 통과하거나 실패할 수 있습니다. CI 러너가 개발자 기기와 다른 프로필을 가지고 있다면, 실패는 플레이크(flake)처럼 보이고 재시도되어 무시될 수 있습니다.

FAQ

이것이 Windows와 Linux에도 영향을 미치나요? 메커니즘은 일반적이지만, 노출 정도는 디스플레이와 캡처 도구에 따라 다릅니다. 이 문제는 캡처가 sRGB가 아닌 넓은 색 영역(wide-gamut) 공간에서 인코딩될 때 나타납니다. 표준 색 영역 디스플레이에서 sRGB로 캡처하는 것은 깔끔하게 왕복 변환되므로, 넓은 색 영역 패널이 노트북에 보편화되기 전에는 이 문제가 드물었습니다.

프로필을 그냥 제거해도 되나요? 아니요. 프로필을 제거해도 아무것도 변환되지 않습니다. 변환에 필요한 정보만 제거할 뿐이며, 그 의미가 이제 문서화되지 않은 숫자만 남게 됩니다. 파일에 프로필이 없으면 픽셀만으로는 원래의 공간을 복구할 수 없습니다.

제 디자인 도구는 스크린샷과 다른 16진수(hex) 값을 보여줍니다. 어느 것이 맞나요? 명시된 공간 없이는 둘 다 맞지 않습니다. 각 숫자가 어떤 공간에 있는지 물어보세요. 디자인 도구는 종종 넓은 색 영역 디스플레이를 통해 미리 보면서 sRGB 값을 표시하는데, 이것이 바로 화면상의 픽셀과 보고된 16진수 값이 일치하지 않는 이유입니다.

이것은 감마(gamma)나 비트 깊이(bit depth)와 같은 것인가요? 아니요. 감마 인코딩과 비트 깊이는 순진한 픽셀 계산을 왜곡할 수 있는 별개의 문제입니다. 이 특정 오류는 숫자가 표현되는 색 공간에 관한 것이며, 올바른 감마 처리와 채널당 8비트를 사용하더라도 계속 발생합니다.

이런 일이 다시 발생하지 않게 하려면 어떻게 해야 하나요? 값 옆에 공간을 명시하세요. 16진수 상수 옆에 sRGB라고 적은 주석이나 샘플의 출처를 기록한 파일 이름은 비용이 들지 않으며 버그를 유발하는 모호성을 제거합니다. 원본 캡처를 프로필과 함께 보관하여, 거기서 파생된 모든 값을 다시 추측하는 대신 재계산할 수 있도록 하세요.