next.js에서 _app의 static chunk가 602kB로 다소 용량이 커서 사용하지 않거나 한 곳에서만 사용하는 라이브러리를 지우는 작업을 진행 중이다. 도중 chroma-js란 라이브러리의 darken
이라는 메소드를 사용하는 코드를 발견하여, 해당 라이브러리와 이 메소드의 동작 원리를 파악하고 이를 대체하는 코드를 만드는 과정을 기록했다.
색상을 변환하는 도구를 제공하는 라이브러리.
https://github.com/gka/chroma.js/issues/217
위 이슈에 따르면 chroma js는 국제 조명 위원회(CIE)에서 정의한 CIEL*a*b*라는 색 공간을 기반으로 메소드를 구현했다고 한다.
chroma-js의 darken method는 CIELab의 L 값을 낮추는 메소드이다.
chroma('hotpink').darken(); // default value = 1
chroma('hotpink').darken(2);
chroma('hotpink').darken(2.6);
// result
// #c93384
// #930058
// #74003f
// chroma.js 내부의 darken 구현 코드
Color$k.prototype.darken = function(amount) {
if ( amount === void 0 ) amount=1;
var me = this;
var lab = me.lab();
lab[0] -= LAB_CONSTANTS$1.Kn * amount;
return new Color$k(lab, 'lab').alpha(me.alpha(), true);
};
// LAB_CONSTANTS$1.Kn = 18
아래와 같이 Chroma라는 클래스를 구현했다.
// Utility functions for color conversion
// (e.g. "03F") to full form (e.g. "0033FF")
const hexShorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
const hexFullRegex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
function expandHexShorthand(hex: string): string {
return hex.replace(hexShorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
}
function hex2Rgb(hex: string) {
hex = expandHexShorthand(hex);
const result = hexFullRegex.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}
function rgb2Hex(rgb: { r: number; g: number; b: number }) {
return (
"#" +
[rgb.r, rgb.g, rgb.b]
.map((value) => value.toString(16).padStart(2, "0"))
.join("")
);
}
// Conversion between RGB and Chroma (L*a*b*) color spaces
function rgb2Chroma(rgb: { r: number; g: number; b: number }) {
const normalize = (v: number) =>
v > 0.04045 ? Math.pow((v + 0.055) / 1.055, 2.4) : v / 12.92;
const r = normalize(rgb.r / 255);
const g = normalize(rgb.g / 255);
const b = normalize(rgb.b / 255);
let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
let y = r * 0.2126 + g * 0.7152 + b * 0.0722;
let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
x = x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116;
y = y > 0.008856 ? Math.pow(y, 1 / 3) : 7.787 * y + 16 / 116;
z = z > 0.008856 ? Math.pow(z, 1 / 3) : 7.787 * z + 16 / 116;
return {
L: 116 * y - 16,
a: 500 * (x - y),
b: 200 * (y - z),
};
}
function chroma2Rgb(chroma: { L: number; a: number; b: number }) {
const y = (chroma.L + 16) / 116;
const x = chroma.a / 500 + y;
const z = y - chroma.b / 200;
const toLinearRgb = (v: number) =>
v > 0.008856 ? Math.pow(v, 3) : (v - 16 / 116) / 7.787;
let xr = 0.95047 * toLinearRgb(x);
let yr = toLinearRgb(y);
let zr = 1.08883 * toLinearRgb(z);
const toRgb = (value: number) =>
value > 0.0031308
? 1.055 * Math.pow(value, 1 / 2.4) - 0.055
: 12.92 * value;
return {
r: Math.round(
Math.max(
0,
Math.min(1, toRgb(xr * 3.2406 + yr * -1.5372 + zr * -0.4986)),
) * 255,
),
g: Math.round(
Math.max(
0,
Math.min(1, toRgb(xr * -0.9689 + yr * 1.8758 + zr * 0.0415)),
) * 255,
),
b: Math.round(
Math.max(0, Math.min(1, toRgb(xr * 0.0557 + yr * -0.204 + zr * 1.057))) *
255,
),
};
}
// Chroma class for managing color transformations
export default class Chroma {
private L: number;
private a: number;
private b: number;
constructor(hex: string) {
const chroma = hex2Chroma(hex);
this.L = chroma?.L ?? 0;
this.a = chroma?.a ?? 0;
this.b = chroma?.b ?? 0;
}
public darken(value: number = 1) {
this.L = Math.max(0, this.L - 18 * value);
return this;
}
public toHex() {
return rgb2Hex(
chroma2Rgb({
L: this.L,
a: this.a,
b: this.b,
}),
);
}
}
// Convert hex to Chroma
function hex2Chroma(hex: string) {
const rgb = hex2Rgb(hex);
return rgb ? rgb2Chroma(rgb) : null;
}
chroma-js 라이브러리를 걷어내어 _app chunk 파일의 크기가 16kB 줄어들었다.
전)
후)