지도의 특정 위치에서 잘 보이는 마커를 표시하기 위하는것을 목표로 하기 위해 다음의 목표를 산정했습니다.
- 좌표로부터 특정 확대 레벨에서의 타일을 가져옵니다
- 타일의 색상 분포를 추출합니다.
- 추출한 색상과 대비되는 색으로 마커의 색을 결정합니다.
그리고, 저번 아티클에서는 1번 단계를 구현하는 내용을 다루었습니다.
3번의 기능을 구현하기 위해서 타 라이브러리를 사용하고 있었지만,
사용중인 라이브러리의 미완성으로 인한 에러가 발생하였고,
이를 해결하기 위해 직접 라이브러리를 구현했습니다.
구현한 라이브러리는 링크에서 확인할 수 있습니다.
이번 아티클에서는 3번의 기능을 구현하기 위해 제작한 라이브러리와 이를 구현하는 과정을 소개해보려 합니다.
이를 해결하기 위해 저는 아래의 방법을 제시했습니다.
- 원하는 기능을 제공하는 라이브러리 탐색 및 비교
- 개념 설명 및 문제 정의
- 라이브러리 개발
- 구현한 기능과 타 라이브러리 성능 비교
처음에는 이러한 기능을 제공하는 라이브러리가 이미 존재할 것이라고 생각했습니다.
이에 npm.js 웹사이트를 뒤적거리며 여러 라이브러리들이 어떠한 방법으로 색을 추출해내는지
코드를 직접 확인한 결과 필요한 기능을 제공해주는 라이브러리가 없는 것을 확인할 수 있었습니다.
아래는 제가 확인한 라이브러리들과, 이들을 사용할 수 없었던 이유입니다.
colord
https://www.npmjs.com/package/colord
colore-js
https://www.npmjs.com/package/colore-js
위의 두 라이브러리는 색을 다루는 경우에 애용되는 라이브러리입니다. 다만, 두 라이브러리 모두 랜덤한 색상을 반환하는 기능은 제공하지만,
a11y 기준을 만족하는 색상을 추천하는 기능은 없었습니다.
react-color-a11y
https://www.npmjs.com/package/react-color-a11y
대비되는 색을 찾을 때 위에서 소개한 colord 라이브러리의 함수를 활용하여 목표 색을 밝게 / 어둡게 하는 방법을 통해 휘도를 조절하여 해결하는 라이브러리 입니다.
const shiftBrightnessUntilTargetLuminence = (originalColord: Colord, targetLuminence: TargetLuminence): Colord => {
let newColord = originalColord
const increment = 0.01
if (targetLuminence.min !== undefined) {
while (newColord.luminance() < targetLuminence.min && newColord.brightness() < 0.99) {
newColord = newColord.lighten(increment)
}
} else if (targetLuminence.max !== undefined) {
while (newColord.luminance() > targetLuminence.max && newColord.brightness() > 0.01) {
newColord = newColord.darken(increment)
}
}
return newColord
}
하지만, 이 라이브러리는 미리 전경색과 배경색을 지정해야 하므로, 필요한 기능과 맞지 않다고 판단했습니다.
accessible-colors
https://www.npmjs.com/package/accessible-colors
위 라이브러리는 발견 당시에는 필요한 함수 그 자체를 가지고 있어서 실제로 짧은 기간 사용했던 라이브러리 입니다.
하지만, 100%확률로 재현할 수 없는 에러가 자주 발생하였고, 이에 코드를 직접 확인한 결과
export const getRandomAAColor = (background: string, large = false): string => {
let color = randomColor();
while (!isAAContrast(background, color, large)) {
color = randomColor();
}
return color;
};
- randomColor함수를 통해 랜덤한 색을 생성하고
- 해당 색이 Contrast값 이상을 가지는지 확인하여
- 대비값이 기준 이상인 경우 return 하는
방법을 사용하고 있었습니다.
하지만 이는 특정 색상이 없거나 (#808080과 같이 7.0 이상의 AAA 대조를 요구하는 경우 만족할 색상이 없는 등), 만족할 수 있는 색상 범위가 매우 좁은 경우 무한 루프에 빠지게 되어 라이브러리를 사용하는 개발자가 의도치 않은 오류를 겪게 될 가능성이 있습니다.
이에 라이브러리 관리자의 github에 issue를 통해 무한루프의 가능성에 대해 알려주었고, 아래와 같이 최대 시도 횟수를 정하여 최대 시도 횟수를 넘어가는 경우에는 null을 return 하는 경우로 코드를 수정하도록 했습니다.
export const getRandomAAColor = (
background: string,
large = false
): string | null => {
let color = randomColor();
let attempts = 0;
while (!isAAContrast(background, color, large)) {
if (attempts++ > 1000) {
return null; // could not find a color that meets the contrast ratio within a reasonable number of tries
}
color = randomColor();
}
return color;
};
하지만, 완전히 random하게 뽑아낸 후 test하는 방법이 근본적인 해결이라고 생각되지 않아서 해당 라이브러리의 사용은 보류하는 것으로 결정했습니다.
a11y-color
https://www.npmjs.com/package/a11y-color
라이브러리의 이름부터 심상치 않은, 근본 가득한 라이브러리같은 기대를 품고 코드를 열어보았는데
업데이트가 9년전 멈춘 코드에는 고대의 문법들이 가득했습니다.
라이브러리의 코드는 9년전 고등학교에서 잠만 자던 범부인 저에게는 너무나도 어려운 내용만 가득했고,
더욱이 절망적으로 github의 repository는 이미 지워진지 오래였습니다.
하지만, 이 라이브러리를 그대로 사용하기에는 라이브러리의 함수를 사용했을 때 결과값의 색상이 잘 보이긴 했지만, 비슷한 색 중, 잘 보이는 색이 반환되는 느낌을 지울 수 없었습니다.
이에 옮겨진 패키지의 레포지토리를 오랜 탐색 끝에 찾을 수 있었고, 해당 라이브러리에서 어느정도 업데이트 된 코드를 통해 대략의 이해를 할 수 있었습니다.
코드를 분석한 결과, 해당 라이브러리는 다음과 같은 방법을 통해 대비되는 색을 추출하고 있었습니다.
- 먼저, 전경색이나 배경색을 YCbCr 색 공간으로 변환합니다.
YCbCr 색 공간은 색의 밝기(휘도)와 색상 성분(Cb, Cr)을 분리하여 표현할 수 있어, 색상을 다루기 편리합니다.- 그다음, 목표로 하는 휘도에 맞추기 위해 YCbCr 색 공간에서 휘도 값을 조정합니다.
이때 색상 성분(Cb, Cr)은 그대로 유지하면서 밝기만을 조절해, 원래 색상의 느낌을 최대한 보존합니다.- 마지막으로, 조정된 YCbCr 색상을 다시 RGB 색 공간으로 변환하여 최종적으로 대비가 잘 맞는 새로운 색상을 생성합니다.
이 과정에서 생성된 색상은 목표 대비 비율을 충족하도록 최적화된 결과입니다.
이 중 2번 단계를 조금 더 자세히 살펴보겠습니다.
let desiredFgLuminance = luminanceFromContrastRatio(bgLuminance, desiredContrast + 0.02, fgLuminanceIsHigher);
코드 중에 위와 같은 부분이 있는데 중간에 0.02를 더해주는 부분이 색상값은 변경을 많이 하지 않으면서 휘도를 유지하는 방법을 통해 자연스러운 색상을 찾기 위한 조정값을 더해주는 부분입니다.
즉, 해당 라이브러리에서 반환하는 색상 값은 휘도는 충분히 변하였지만, 색상은 비슷한 느낌을 주는 대비색을 찾아서 반환하는 라이브러리 인 것입니다.
필요한 기능은 자연스러운 색이 아닌, 눈에 잘 띄는 색이였기에 해당 라이브러리의 사용을 포기했습니다.
여러 라이브러리를 분석한 결과, 지도의 특정 위치에서 잘 보이는 마커를 표시하기 위한 요구 사항을 충족하는 완벽한 솔루션이 없다는 결론에 이르렀습니다.
이에 직접 라이브러리를 개발해야겠다는 결론을 내렸습니다.
두 색의 대비값은 어렵지 않게 계산을 통해 구해낼 수 있습니다. (이는 아래의 개념 설명과 계산 예시에서 자세히 다루겠습니다)
하지만, 한 색으로부터 대비되는 색을 추출해내는것은 어떻게 할 수 있을까요?
이에 아래와 같이 필요 기능 및 문제를 정의했습니다.
- 색과 대비 비율이 주어질 때 이를 만족하는 색을 추출할 수 있다.
- 실행 결과 기존의 무작위 추출 후 검증하는 라이브러리보다 높은 정확성을 보인다
- 실행 결과 기존의 무작위 추출 후 검증하는 라이브러리보다 빠른 속도를 보인다
색(color)은 RGB(A), HSV, YCbCr, CMYK 등의 여러 약속된 방법을 통해 나타낼 수 있습니다.
이는 각각의 색 공간을 의미하고, 각 공간 사이에는 여러 방법을 통해 변환이 이루어질 수 있습니다. (같은 색을 나타내는 서로 다른 방법이 있는 것 이니까요)
각각의 색 공간은 다음의 특징을 가지고 있습니다.
색 공간은 색상을 표현하는 방법을 정의하는 시스템으로, 여러 가지가 존재합니다. 각각의 색 공간은 특정한 용도와 목표를 염두에 두고 설계되었습니다.
색상 대비는 두 색상이 서로 얼마나 구별될 수 있는지를 나타내는 지표입니다. 이 대비는 텍스트를 읽기 쉽게 만들거나 특정 요소를 눈에 잘 띄게 하는 데 필수적인 요소입니다. 색상 대비를 계산할 때는 주로 휘도 대비를 기준으로 합니다.
휘도 대비 (Luminance Contrast):
휘도 대비는 두 색상의 휘도(Luminance) 차이를 기반으로 계산되며, 일반적으로 1:1(같은 휘도)에서 21:1(최대 대비)까지의 범위로 나타냅니다.
이 비율은 색상 대비를 정량적으로 표현하는 데 사용되며, 웹 콘텐츠 접근성 지침(WCAG, Web Content Accessibility Guidelines)에 따르면 텍스트와 배경 간의 최소 대비 비율은 다음과 같습니다:
- 일반 텍스트: 최소 4.5:1 (AA 기준)
- 큰 텍스트 또는 UI 컴포넌트: 최소 3:1 (AA 기준)
이 기준을 충족하는지 확인하려면 WCAG 대조 계산기를 사용할 수 있습니다.
휘도 (Luminance):
휘도는 색상의 밝기를 측정하는 값으로, 색상 대비를 계산하는 데 중요한 역할을 합니다. 휘도는 RGB 색상 값에 따라 계산되며, 각 색상의 가중치를 반영하여 특정 공식을 통해 계산됩니다. 이 공식은 인간의 시각이 특정 색상(예: 녹색)에 더 민감하게 반응한다는 점을 고려하여 만들어졌습니다. 휘도를 계산하는 일반적인 공식은 다음과 같습니다:
여기서 R, G, B는 각각 빨강, 녹색, 파랑의 색상 값(0에서 255 사이)를
- 정규화: [0,255]의 색상값을 [0,1]으로 변환
- 선형화: 가감마된 색상 공간의 값을 선형화
- 휘도 계산:
L = 0.2126 * R + 0.7152 * B + 0.0722 * G
를 사용하여 휘도 계산
의 단계를 거쳐서 휘도값을 계산합니다.
이때 사용된 가중치(0.2126, 0.7152, 0.0722)는 각각 빨간색, 초록색, 파란색에 대한 인간의 시각 민감도를 반영한 것입니다. 이 값들은 국제표준으로 정의된 sRGB 색 공간의 특성에 기반합니다.
여러 색 공간과 색상 대비를 할 때 사용하는 지표가 있지만, 저는 RGB와 휘도 대비를 사용하였습니다.
직접 계산을 해보면서 예시를 들어보겠습니다.
계산에는 아래의 색을 사용합니다.
색의 휘도를 계산하기 위해 색의 각 RGB값을 linear한 값으로 변환해야합니다.
각 색의 휘도를 계산합니다. 이를 위해 L = 0.2126 * R + 0.7152 * G + 0.0722 * B
의 공식을 사용합니다.
휘도 대비를 계산합니다.
https://webaim.org/resources/contrastchecker/?fcolor=B478FF&bcolor=0A0A0A
두 색은 훌륭한 6.67의 대비값을 보이고 있습니다.
3.0 이상의 대비값을 보이며 WCAG의 기준을 충족하며, 동시에 4.5 이상의 대비값으로 작은 글씨에서도 기준을 만족하고 있습니다.
문제 정의는 세줄이지만, 사실 해야할 일은 단 하나입니다
“색과 대비 비율이 주어질 때 이를 만족하는 색을 추출하는 라이브러리를 잘(정확하고 빠르게) 만든다”
색과 대비 비율이 주어질 때 라고 했으니 이를 파라미터로 입력받을 수 있도록 해야겠죠
이를 위해 RGB type을
export type RGB = [number, number, number];
와 같이 선언해주었고, 대비 정도는 multiplier
변수로 사용했습니다.
getLuminance
함수가 시작하면 냅다 배경색의 휘도를 구해야합니다. 모든 계산은 휘도를 기반으로 사용되고,
휘도는 함수 내부에서 임의로 사용할 값이 아닌, 이미 정해져있는 개념이므로 get~~
의 문법을 사용하여
해당 라이브러리를 import할때 활용할 수 있도록 네이밍했습니다.
export const normalizeElement = (element: number): number => {
return element / 255;
};
export const linearizeElement = (element: number): number => {
return element <= 0.03928
? element / 12.92
: Math.pow((element + 0.055) / 1.055, 2.4);
};
export const RGB8bitToLinear = (channelValue: number): number => {
if (channelValue < 0 || channelValue > 255) {
throw new Error('Invalid 8-bit RGB channel value.');
}
// 🎨 [0,255]의 값을 [0,1] 정규화
const normalizedValue = normalizeElement(channelValue);
// 🎨 8bit RGB를 Linear하게 선형화
const linearizedValue = linearizeElement(normalizedValue);
return linearizedValue;
};
export const getLuminance = (RGBValue: RGB): number => {
const LinearRGB = RGBValue.map(RGB8bitToLinear);
// 🎨 가중치를 곱하여 휘도 계산
return 0.2126 * LinearRGB[0] + 0.7152 * LinearRGB[1] + 0.0722 * LinearRGB[2];
};
위의 함수를 사용하면 input으로 주어지는 배경색의 휘도를 계산할 수 있습니다.
calculateBounds
우리는 대비 비율이 multiplier
이상인 값을 찾아야 합니다.
RGB 색 공간에서는 색이 섞일수록 밝아지기 때문에, 밝은 색의 휘도는 어두운 색의 휘도보다 항상 큰 값을 가집니다. 즉 위의 수식은 아래와 같습니다.
배경색의 휘도 값을 k라고 하면, 배경이 더 밝은 경우와 어두운 경우에 대해 아래와 같은 수식을 도출할 수 있습니다.
i) 배경이 밝은 경우
ii) 배경이 어두운 경우
즉, 배경이 밝은 경우와 어두운 경우 각각 upperbound
와 lowerbound
를 두고 양 끝값의 범위에서 휘도를 구할 수 있음을 의미합니다.
위의 내용을 함수로 구현하면 다음과 같습니다.
const calculateBounds = (
multiplier: number,
luminance: number
): [number, number] => {
const lowerBound = luminance / multiplier + 0.05 * (1 / multiplier - 1);
const upperBound = multiplier * luminance + 0.05 * (multiplier - 1);
return [Math.max(0, lowerBound), Math.min(1, upperBound)];
};
휘도 범위를 구하는 함수의 경우는 해당 라이브러리에서만 사용할 개념입니다.
그렇기 때문에 get~
의 네이밍을 사용하지 않고, caculate~
의 네이밍을 통해 내부 로직임을 표현했습니다.
(모듈화된 타 함수들의 경우에도 get~
의 네이밍 컨벤션의 함수만이 라이브러리를 import했을 때 사용할 수 있는 함수입니다)
calculateTotalLength, generateTargetLuminance
[그림 4-1]의 그래프에서 보라색 영역에 해당하는 휘도를 선택하는 방법에 대해 고민해 보았습니다.
휘도 차이가 딱 multiplier
만큼 나는 부분을 고르면 되지 않나?
이 방법은 직관적으로 매력적입니다. 하지만, 대비 효과가 딱 multiplier
여야만 하는 특별한 이유가 없었습니다.
오히려, 가능한 한 대비 효과를 (적당한 범위 내에서) 극대화 하는 것이 바람직한 상황이었기 때문에,
최소한의 multiplier
이상의 휘도 값을 보장하는 값을 선택하기로 했습니다.
그러면 휘도 차이가 최대한 많이 나는 부분을 고르면 되지 않을까?
이 선택지는 곧바로 폐기했습니다. 이유는 명확합니다.
휘도 차이가 가장 큰 값은 검은색([0,0,0])이거나, 흰색([255,255,255]) 둘 중 하나의 색임에 분명했죠.
이 둘 중 하나를 반환하는 것은 색상 대비를 뚜렷하게 만들기는 하지만, 애초에 이런 간단한 방법을 사용할 것이었다면,
애초에 라이브러리를 만들 생각도 하지 않았을 것 입니다.
그럼 남은 결론은 단 하나, random한 휘도 값!
최종적으로, [그림 4-1]의 보라색 영역 안에서 무작위(random)로 휘도 값을 선택하는 것이 가장 적절하다고 결론지었습니다.
범위 내에서 무작위한 값을 선택하기 위해 Math.rand()
매서드를 사용했습니다. 하지만, rand()
매서드는 그림 4-2의 (1)처럼 [0,1)
의 결과값을 return합니다
우리는 (2) 에 해당하는 calculateBounds()
함수로부터 추출한 보라색 영역안의 임의의 영역을 선택해야하기 때문에
(3)과 같이 범위를 모아주고, 모은 전체 범위의 길이를 return하는 calculateTotalLength
함수를 제작했습니다.
그러면 우리는 [0,1)
의 값을 return하는 rand()
에 calculateTotalLength()
를 곱해주면 (4) 보라색 범위 내의 영역에서 random한 값을 선택할 수 있게 됩니다.
해당 부분의 코드는 아래와 같습니다.
const calculateTotalLength = (bounds: number[]):number => {
const lowerBound = bounds[0];
const upperBound = bounds[1];
return lowerBound + 1 - upperBound;
};
무작위 영역은 위의 함수를 통해서 고를 수 있지만, 고른 부분은 (4)와 같이 빨간색의 영역으로 연속된 공간에 있습니다.
하지만, 실제 선택해야하는 휘도의 boundary는 (2)의 보라색 영역과 같이 불연속하게 위치해있습니다.
이를 위해 삼항연산자를 사용하여 lowerBound와 (4)에서 추출한 임의의 값과 비교하여
lowerBound보다 작은 경우에는 해당 값을 그대로 반환하고,
lowerBound보다 큰 경우에는 값을 보라색 우측 영역으로 이동시켜주는 로직을 추가했습니다.
이는 아래와 같은 코드를 사용하여 구현했습니다.
const generateTargetLuminance = (
totalLength: number,
lowerBound: number,
upperBound: number,
weight: number
): number => {
const _tmp = Math.random() * totalLength * weight;
return _tmp > lowerBound ? _tmp + (-lowerBound + upperBound) * weight : _tmp;
};
여기서 weight
를 곱해주는 이유는 각 RGB 채널의 가중치를 반영하기 위해서입니다. 만약 weight
를 사용하지 않으면,
각 채널에서 선택된 휘도 값이 전체적으로 일관된 색상 범위를 형성하지 못할 수 있습니다.
이는 각 채널의 기여도가 다르기 때문에, 각 채널에서 선택된 값이 균등한 확률로 선택되면, 시각적으로 부자연스럽거나 잘못된 색상 조합이 생성될 수 있습니다.
하지만, weight
를 곱해줌으로써 각 채널의 휘도 값이 올바르게 가중된 상태에서 선택되며, 이를 통해 인간의 시각적 경험이 반영된 색상 범위를 선택할 수 있었습니다.
calculateContrastColorInnerLogic, getContrastColor
드디어 모든 모듈 함수들의 제작이 끝났습니다. 이제 전체적인 함수가 모듈화된 함수들을 순서에 맞게 합쳐주기만 하면 되겠네요!
calculateContrastColorInnerLogic
이 함수는 주어진 RGB 색상(color
)과 목표 휘도(luminance
)에 대해, 해당 색상보다 더 큰 대비를 갖는 색상을 찾는 내부 로직을 담당합니다.
구체적으로 이 함수는 다음과 같은 단계를 거칩니다:
- 기본 색상의 휘도 계산: 먼저,
getLuminance
함수를 통해 입력된 색상(color
)의 배경 휘도(backgroundLuminance
)를 계산합니다.
이 값은 색상 대비를 계산하는 기초가 됩니다.- 휘도 범위 계산:
calculateBounds
함수를 사용하여 목표 휘도(luminance
)와 배경 휘도(backgroundLuminance
)를 바탕으로
휘도 범위([lowerBound, upperBound]
)를 계산합니다. 이는 해당 색상이 가질 수 있는 휘도의 하한과 상한을 의미합니다.- 전체 범위 길이 계산:
calculateTotalLength
함수를 사용해 계산된 휘도 범위 내에서 사용할 수 있는 전체 길이를 계산합니다.
이 길이는 무작위로 휘도 값을 선택할 때 기준이 됩니다.- 채널별 무작위 휘도 생성:
generateTargetLuminance
함수는 무작위한 휘도 값을 생성합니다.
각 RGB 채널(빨강, 녹색, 파랑)마다 이 값을 개별적으로 계산하여, 각 채널의 가중치(weight
)를 반영한 무작위 휘도 값을 얻습니다.- 색상 조합: 각 채널에서 선택된 무작위 휘도 값을 바탕으로 선형 RGB 값을 계산하고(
LinearRGBfromLuminance
),
이를 다시 8비트 RGB 값으로 변환하여 최종 색상을 구성합니다(RGBLinearTo8bit
).
이 과정은 각 RGB 채널의 휘도 값이 자연스럽고 일관된 색상 범위를 형성하도록 하며, 최종적으로 주어진 목표 휘도보다 더 큰 대비를 가지는 색상을 생성합니다.
해당 함수의 코드는 아래와 같이 작성했습니다.
export const calculateContrastColorInnerLogic = (
color: RGB,
luminance: number
): RGB | null => {
// 🎨 기본 색상의 휘도 계산
let backgroundLuminance = getLuminance(color);
// 🎨 휘도 범위 계산 (Red 채널)
let [lowerBound, upperBound] = calculateBounds(
luminance,
backgroundLuminance
);
// 🎨 전체 범위 길이 계산 (Red 채널)
let totalLength = calculateTotalLength([lowerBound, upperBound]);
// 🎨 채널별 무작위 휘도 생성 (Red 채널)
let _targetLuminance = generateTargetLuminance(
totalLength,
lowerBound,
upperBound,
RGBChannelWeights.R
);
const LinearR = LinearRGBfromLuminance(_targetLuminance, 'R');
const _8bitR = RGBLinearTo8bit(LinearR);
backgroundLuminance -= _targetLuminance;
// 🎨 휘도 범위 계산 (Green 채널)
[lowerBound, upperBound] = calculateBounds(luminance, backgroundLuminance);
// 🎨 전체 범위 길이 계산 (Green 채널)
totalLength = calculateTotalLength([lowerBound, upperBound]);
// 🎨 채널별 무작위 휘도 생성 (Green 채널)
_targetLuminance = generateTargetLuminance(
totalLength,
lowerBound,
upperBound,
RGBChannelWeights.G
);
const LinearG = LinearRGBfromLuminance(_targetLuminance, 'G');
const _8bitG = RGBLinearTo8bit(LinearG);
backgroundLuminance -= _targetLuminance;
// 🎨 휘도 범위 계산 (Blue 채널)
[lowerBound, upperBound] = calculateBounds(luminance, backgroundLuminance);
// 🎨 전체 범위 길이 계산 (Blue 채널)
totalLength = calculateTotalLength([lowerBound, upperBound]);
// 🎨 채널별 무작위 휘도 생성 (Blue 채널)
_targetLuminance = generateTargetLuminance(
totalLength,
lowerBound,
upperBound,
RGBChannelWeights.B
);
const LinearB = LinearRGBfromLuminance(_targetLuminance, 'B');
const _8bitB = RGBLinearTo8bit(LinearB);
// 🎨 색상 조합
return [_8bitR, _8bitG, _8bitB];
};
getContrastColor
이 함수는 calculateContrastColorInnerLogic
을 활용하여 주어진 색상(color
)과 목표 휘도(luminance
)에 대해, 그보다 높은 대비를 가지는 색상을 찾습니다.
외부에서 호출되는 인터페이스로, 사용자에게 최종 대비 색상을 제공하는 역할을 합니다.
구체적인 작동 방식은 다음과 같습니다:
- 입력 값 검증:
validateColor
와validateLuminance
함수를 사용해 입력된 RGB 색상 배열과 휘도 값이 유효한지 확인합니다.
잘못된 값이 들어오면 예외를 발생시킵니다.- 반복적인 색상 탐색: 내부적으로 1000번의 반복을 통해
calculateContrastColorInnerLogic
을 호출하여 무작위로 색상을 생성하고,
그 색상이 목표 휘도보다 높은 대비를 가지는지 확인합니다. 만약 조건을 충족하는 색상이 발견되면, 해당 색상을 반환합니다.- 예외 처리: 만약
calculateContrastColorInnerLogic
함수에서 발생하는 예외가 ”Invalid linear RGB value”일 경우,
그 색상은 무시하고 다음 반복으로 넘어갑니다. 다른 예외가 발생하면 해당 예외를 다시 던집니다.- 적합한 색상 찾기 실패: 만약 1000번의 시도 내에 조건을 충족하는 색상을 찾지 못하면,
null
을 반환하여 적절한 색상을 찾지 못했음을 알립니다.
이 함수는 사용자에게 명확한 목표 대비를 달성할 수 있는 색상을 제공하며, 만약 목표를 달성하지 못할 경우에도 안전하게 처리되도록 설계되어 있습니다.
해당 로직은 아래와 같은 코드로 작성했습니다.
export const getContrastColor = (
color: number[],
luminance: number
): RGB | null => {
// 🎨 입력 값 검증
validateColor(color as RGB);
validateLuminance(luminance);
const bgColor = color as RGB;
let i = 0;
while (i < 1000) {
i++;
try {
// 🎨 반복적인 색상 탐색
const targetColor = calculateContrastColorInnerLogic(bgColor, luminance);
if (getContrastRatio(targetColor!, bgColor) > luminance) {
return targetColor;
}
} catch (e: any) {
// 🎨 예외 처리
if (e.message === 'Invalid linear RGB value') {
continue;
} else {
throw e;
}
}
}
// 🎨 적합한 색상 찾기 실패
return null;
};
아래는 성능 비교를 위해 같은 기능을 제공하는 타 라이브러리를 import 하여 테스트 코드를 작성하여 30회정도 테스트 한 평균을 나타낸 테이블입니다.
내부 | 외부 | |
---|---|---|
평균 | 1.78ms | 3.89ms |
최소시간 | 1ms | 2ms |
최대시간 | 8ms | 6ms |
위의 개발 과정을 통해 구현한 코드는 빌드하여 오픈소스로 깃허브에 공개해두었고, 패키지는 배포하여 업로드하였습니다.
데모 링크 : https://a11y-contrast-color.vercel.app/
라이브러리 링크 : https://www.npmjs.com/package/a11y-contrast-color
깃허브 레포지토리 : https://github.com/moong23/a11y-contrast-color
이번 성능 비교를 통해 직접 개발한 라이브러리가 외부 라이브러리와 비교했을 때 평균적으로 더 빠르게 동작한다는 것을 확인할 수 있었습니다.
특히, 특정 상황에서는 외부 라이브러리와 동일한 iteration을 하는 조건에서도 열 배 이상 빠른 성능을 보이기도 했습니다.
이러한 성능 향상은 사용자 경험을 향상시키고, 지도에서 마커를 보다 명확하게 표시하는 데 중요한 역할을 할 수 있을것으로 기대가 됩니다.
따라서, 이번 프로젝트에서 제시한 목적에 맞게 지도의 특정 위치에서 잘 보이는 마커를 표시하기 위해 자체 개발한 라이브러리를 사용하는 것이 더욱 유리하다고 판단됩니다.
해당 라이브러리는 오픈소스 프로젝트로, 지속적인 관리를 통해 색상을 추출하는 알고리즘을 유지보수하도록 하고,
더 나은 알고리즘이 있다면 교체하여 최선의 색상을 선택하는데 도움이 되도록 할 예정입니다.
이로써, 이번 아티클에서 다루고자 했던 내용들을 모두 마무리하며, 다음 단계에서는 다시 원래의 아티클로 돌아가서 타일으로부터 색상 분포를 추출하고,
추출한 색으로부터 해당 라이브러리를 사용하여 눈에 잘 보이는 마커를 표시하는 과정에 대한 아티클을 이어가겠습니다.
감사합니다!
https://www.w3.org/TR/WCAG20/relative-luminance.xml
https://www.w3.org/TR/WCAG20/#contrast-ratiodef
https://www.w3.org/WAI/older-users/developing/#color
https://www.npmjs.com/package/colord
https://www.npmjs.com/package/colore-js
https://www.npmjs.com/package/react-color-a11y
https://www.npmjs.com/package/accessible-colors
https://www.npmjs.com/package/a11y-color