Colbrush: 접근성 문제를 라이브러리로 풀어낸 과정
1. 왜 웹앱이 아니라 라이브러리였나?
Colbrush는 오픈소스 개발자대회 사회문제 전형에 참가하면서 시작한 프로젝트입니다.
처음부터 라이브러리를 만들겠다고 정해두고 출발한 것은 아니었습니다.
여러 아이디어를 검토했지만, 구현 가능성과 문제의 확장성을 함께 봤을 때 가장 설득력 있었던 주제는 색상 접근성이었습니다.
색각 이상 사용자는 색상만으로 상태를 구분하는 UI에서 중요한 정보를 놓칠 수 있습니다.
예를 들어 성공과 실패, 활성과 비활성, 위험과 안전 같은 상태가 색으로만 표현된다면 일부 사용자는 그 차이를 명확히 인식하기 어렵습니다.
문제는 이것이 특정 서비스 하나만의 문제가 아니라는 점이었습니다.
서비스마다 같은 문제를 다시 해결해야 하고, 그 비용 때문에 접근성은 종종 우선순위에서 밀립니다.
그래서 저는 “접근성을 고려한 웹앱 하나”보다, 개발자가 자신의 프로젝트에 바로 붙여 쓸 수 있는 도구를 만드는 편이 더 의미 있다고 판단했습니다.
Colbrush는 그 판단에서 시작된 npm 배포형 React 라이브러리입니다.
2. 목표: 원래 디자인을 해치지 않으면서도 구분 가능하게
Colbrush의 목표는 단순히 색을 바꾸는 것이 아니었습니다.
팀 안에서 세운 기준은 세 가지였습니다.
- 색각 이상 사용자도 구분이 필요한 색을 충분히 구별할 수 있어야 한다.
- 원래 디자인과 완전히 동떨어진 팔레트가 되어서는 안 된다.
- 처음부터 비슷한 계열로 정의된 색은 과하게 벌리지 않고, 상태 색상처럼 실제로 구분이 필요한 색 위주로 차이를 확보해야 한다.
이를 위해 팀 내부에서는 Delta E 12 이상을 기준으로 삼았습니다.
색상값 자체가 얼마나 다른지가 아니라, 사람이 느끼기에 얼마나 다르게 인식되는지를 기준으로 접근하고 싶었기 때문입니다.
여기서 기준으로 삼은 색공간은 LAB였습니다.
RGB가 화면에 색을 표시하기 위한 표현에 가깝다면, LAB는 사람이 색을 인지하는 방식에 더 가깝게 설계된 색공간입니다. L*은 밝기, a*는 초록-빨강 축, b*는 파랑-노랑 축을 나타냅니다.
적색맹 시야를 시뮬레이션하면 특히 a* 축, 즉 초록-빨강 방향의 구분이 크게 약해집니다.
원래는 분명히 다른 색으로 보이던 영역도 비슷한 노랑/갈색 계열처럼 뭉쳐 보일 수 있습니다.
Colbrush에서 하려던 일은 이 차이를 무작정 크게 벌리는 것이 아니었습니다.
원래 디자인의 색감은 가능한 유지하되, 구분이 필요한 색 사이의 지각 거리를 확보하는 쪽에 가까웠습니다.
이 방향을 잡을 때 참고한 자료는 김용근의 「적록 색각 이상자를 위한 색 공간 변환」 논문이었습니다.
이 논문은 적록 색각 이상자를 위해 RGB 채널과 CIE L*a*b* 색공간을 변환하는 여러 방식을 다룹니다. Colbrush에서도 이 관점을 가져와, 색을 단순히 “다른 색”으로 바꾸기보다 색각 이상 환경에서 구분이 약해지는 축을 확인하고 보정하는 방향으로 접근했습니다.
접근성을 이유로 디자인을 완전히 무너뜨리면 실제 프로젝트에 적용되기 어렵습니다.
반대로 기존 디자인을 너무 보존하려고 하면 접근성 개선 효과가 약해집니다.
Colbrush는 이 두 기준 사이에서 현실적으로 사용할 수 있는 균형점을 찾는 프로젝트였습니다.
3. 내가 맡은 역할
제가 맡은 범위는 라이브러리의 핵심 동작과 외부 사용 흐름에 가까웠습니다.
- 글로벌 스타일 파일 파싱
- 기준 색상으로부터
50 ~ 900variation 생성 - CLI 명령어 생성 로직과 메인 실행 흐름 구현
ThemeProvider,ThemeSwitcher구현- 데모 페이지의 사용법 섹션 구현
이 중 가장 중요하게 생각한 부분은 “라이브러리를 실제 프로젝트에서 어떻게 쓰게 만들 것인가”였습니다.
웹앱은 내 환경에서만 잘 돌아가도 어느 정도 결과를 보여줄 수 있습니다.
하지만 라이브러리는 다릅니다. 사용자의 프로젝트 구조, 번들러, 스타일링 방식, 렌더링 환경이 모두 다를 수 있습니다.
내 프로젝트에서는 정상적으로 보이던 코드가, 다른 프로젝트에 들어가는 순간 충돌하거나 깨질 수 있습니다.
그래서 Colbrush를 개발하면서는 기능 구현만큼이나 “외부 프로젝트에서 안전하게 소비될 수 있는가”를 계속 확인해야 했습니다.
4. 색상 토큰을 읽고, 외부 프로젝트가 바로 쓸 수 있게 만들기
Colbrush는 기존 CSS 변수를 파싱해 색상 토큰을 읽고, 이를 기반으로 색각 유형별 테마를 생성하는 구조입니다.
사용자는 이미 프로젝트 안에서 색상 토큰을 관리하고 있을 가능성이 높습니다.
따라서 Colbrush가 별도의 색상 체계를 강요하기보다는, 기존에 정의된 CSS 변수를 읽고 접근성 기준에 맞춰 변환하는 방식이 더 자연스럽다고 판단했습니다.
저는 이 과정에서 색상 데이터를 변환하는 로직과, 실제 앱에서 테마 상태를 유지하는 런타임 레이어를 함께 다뤘습니다.
ThemeProvider는 테마, 언어, 시뮬레이션 상태를 묶어 관리합니다.
그리고 이 상태를 localStorage와 document.documentElement에 반영해 앱 전역에서 일관되게 사용할 수 있도록 만들었습니다.
ThemeSwitcher는 이 상태를 사용자 인터페이스로 노출하는 역할을 했습니다.
개발자가 라이브러리를 설치한 뒤 바로 테마를 바꾸고, 색각 유형별 시뮬레이션을 확인할 수 있어야 했기 때문입니다.
즉, Colbrush는 단순히 “색상을 계산하는 도구”가 아니라, 계산된 결과를 실제 프로젝트에서 적용하고 체험할 수 있는 구조까지 포함한 라이브러리였습니다.
5. 실제 화면에서 검증한 테마 적용 전후
색상 접근성은 코드만으로 설명하기 어렵습니다.
수치상으로 Delta E가 충분히 벌어졌다고 해도, 실제 UI에서 정보가 구분되는지는 별도로 확인해야 했습니다.
그래서 데모 페이지에는 색각 유형별 테스트 카드를 배치했습니다.
각 카드는 단순히 색상 팔레트를 보여주는 용도가 아니라, 테마 변경 후에도 중요한 형태와 대비가 유지되는지 확인하기 위한 기준 화면에 가까웠습니다.
왼쪽은 기본 테마 상태입니다.
시뮬레이션 필터는 tritanopia로 켜져 있지만, 실제 활성 테마는 아직 default입니다. 이 상태에서는 청색맹 시야를 가정했을 때 파랑-초록, 노랑-분홍 계열의 구분이 약하게 보입니다.
오른쪽은 tritanopia 테마를 적용한 뒤입니다.
동일한 시뮬레이션 조건에서 원형 패턴과 배경의 대비가 더 분명해지고, 카드 안의 정보가 이전보다 안정적으로 구분됩니다.
이 화면을 만들면서 중요하게 본 지점은 “예쁜 팔레트”가 아니라 “구분 가능한 상태”였습니다.
테마가 적용된 뒤에도 기존 UI의 분위기를 크게 해치지 않으면서, 색각 이상 사용자가 놓칠 수 있는 정보를 줄이는 것이 목표였습니다.
6. 트러블슈팅 1: SVG 아이콘을 런타임 변환에 맡기지 않기
가장 먼저 부딪힌 문제는 ThemeSwitcher 내부 SVG 아이콘이 빌드 환경에 따라 깨지거나 사라지는 현상이었습니다.
대표적으로는 이런 문제가 있었습니다.
viewBox가 삭제되면서 아이콘 비율이 깨짐?reactimport 방식이 환경에 따라 일관되지 않음- 데모에서는 보이던 아이콘이 라이브러리 빌드 후에는 정상적으로 렌더링되지 않음
처음에는 단순한 Vite 설정 문제처럼 보였습니다.
하지만 확인해보니 SVGO 기본 최적화와 SVG 런타임 변환 방식이 함께 얽혀 있었습니다.
라이브러리에서는 사용자의 번들러 환경을 예측하기 어렵습니다.
그래서 런타임에서 SVG를 변환하는 방식에 기대기보다, 빌드 전에 SVG를 .tsx 컴포넌트로 변환하는 쪽으로 방향을 바꿨습니다.
// svgr.config.cjs
module.exports = {
typescript: true,
svgo: true,
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: { overrides: { removeViewBox: false } },
},
{ name: 'removeDimensions', active: true },
],
},
};이렇게 전환한 뒤에는 아이콘 import가 안정화됐고, 팀 내에서도 아이콘 처리 규칙을 명확히 공유할 수 있었습니다. 이 경험을 통해 라이브러리에서는 “내 개발 환경에서 우연히 잘 되는 방식”보다 “빌드 결과물이 예측 가능한 방식”이 더 중요하다는 것을 배웠습니다.
7. 트러블슈팅 2: Tailwind를 두 번 불러오면 사용자 프로젝트가 깨진다
라이브러리 배포 후 받은 가장 치명적인 피드백은 이것이었습니다.
“Colbrush를 설치하니까 기존 프로젝트의 반응형 스타일이 깨집니다.”
처음에는 Colbrush의 컴포넌트 스타일이 사용자 프로젝트와 충돌하는 줄 알았습니다.
하지만 원인은 더 근본적인 곳에 있었습니다.
라이브러리 내부 스타일 파일에서 @import 'tailwindcss'를 다시 호출하고 있었던 것입니다.
사용자 프로젝트도 이미 Tailwind를 사용하고 있는데, 라이브러리에서 다시 Base Layer를 주입하면서 스타일 레이어 순서가 꼬였습니다.
그 결과 Colbrush와 직접 관련 없는 기존 반응형 스타일에도 영향을 주게 됐습니다.
이 문제를 해결하면서 배운 점은 분명했습니다.
라이브러리는 “내 프로젝트에서 편한 구조”가 아니라, “사용자 환경과 최대한 충돌하지 않는 구조”가 우선이어야 합니다.
결국 라이브러리 내부에서 Tailwind 의존성을 제거했습니다.
styles.css에는 우리가 직접 생성한 CSS 변수와 최소한의 레이아웃 스타일만 남겼습니다.
그 결과 Colbrush는 특정 스타일링 도구에 덜 묶인 형태가 됐고, 사용자 프로젝트의 Tailwind 설정을 침범하지 않게 됐습니다.
이 문제는 라이브러리를 만들 때 가장 조심해야 하는 부분을 알려줬습니다.
라이브러리 내부에서 당연하게 쓰던 도구가, 사용자 프로젝트에서는 전역 충돌의 원인이 될 수 있습니다.
8. 트러블슈팅 3: 로컬에서는 되는데 번들 후엔 깨지는 CJS/MJS 문제
이 프로젝트에서 가장 많이 배운 지점은 사실 색상 알고리즘보다 번들링이었습니다.
2차 개발 과정에서 Colbrush는 더 다양한 색상 표기법을 지원하도록 확장하고 있었습니다.
처음에는 hex 중심으로 동작하던 로직을 rgb, rgba, oklch 같은 표기법까지 처리할 수 있게 만들고 싶었습니다.
이를 위해 색상 파싱과 변환에 colorjs.io를 사용했습니다.
로컬 개발 환경에서는 문제가 없어 보였습니다. 개발 서버에서는 색상값이 정상적으로 파싱됐고, 변환된 색상도 기대한 형태로 생성됐습니다.
그래서 처음에는 기능 구현 자체는 끝났다고 생각했습니다.
하지만 라이브러리를 빌드한 뒤 CommonJS 환경에서 소비했을 때 런타임 에러가 발생했습니다.
같은 코드인데 개발 환경에서는 되고, 배포된 번들에서는 깨지는 상황이었습니다.
처음에는 색상 파싱 로직의 문제라고 생각했습니다.
rgb()나 oklch() 문자열을 처리하는 과정에서 예외 케이스를 놓쳤다고 의심했습니다.
하지만 입력값을 단순화해도 에러는 계속 발생했고, 문제는 색상값이 아니라 Color 생성자를 가져오는 방식에 있었습니다.
핵심은 ESM과 CommonJS 번들 사이의 모듈 interop 문제였습니다.
colorjs.io는 ESM 기반으로 사용하는 라이브러리였고, Colbrush는 배포를 위해 ESM과 CJS 번들을 함께 생성하고 있었습니다.
이 과정에서 tsup이 CommonJS 번들을 만들면서 import 결과를 원래 기대한 생성자 함수가 아니라 { default: ColorCtor } 형태로 감싸는 경우가 생겼습니다.
즉, 로컬에서는 다음과 같은 방식이 자연스럽게 동작했습니다.
import Color from 'colorjs.io';
const color = new Color(value);하지만 번들된 CommonJS 결과물에서는 런타임에서 실제로 잡히는 값이 생성자 자체가 아니라 default 프로퍼티를 가진 객체에 가까웠습니다.
{
default: ColorCtor
}그 상태에서 기존 코드처럼 바로 new Color(value)를 호출하면, 런타임 입장에서는 생성자가 아닌 값을 생성자로 호출하는 셈이 됩니다.
그래서 로컬에서는 멀쩡하던 코드가, 빌드 결과물을 CJS 환경에서 소비하는 순간 깨졌습니다.
이 문제를 해결하면서 가장 먼저 한 일은 “소스 코드”가 아니라 “빌드된 결과물”을 확인하는 것이었습니다.
이전까지는 TypeScript 소스와 개발 서버 기준으로만 문제를 판단했는데, 라이브러리에서는 실제 사용자가 보는 코드가 src가 아니라 dist라는 점을 다시 확인하게 됐습니다. 특히 npm 패키지로 배포되는 라이브러리는 사용자의 프로젝트에서 다양한 방식으로 소비됩니다.
- ESM 환경에서 import될 수 있다.
- CommonJS 환경에서 require될 수 있다.
- 번들러가 한 번 더 감쌀 수 있다.
- SSR 환경에서 실행될 수도 있다.
따라서 “내 로컬 개발 서버에서 잘 된다”는 것은 라이브러리에서는 충분한 검증이 아니었습니다. 개발 환경은 보통 소스 코드에 가깝고, HMR과 번들러 설정도 프로젝트 내부 기준으로 맞춰져 있습니다.
반면 사용자는 빌드된 패키지를 설치하고, 자신의 번들러나 런타임 환경에서 실행합니다. 이 차이를 놓치면 라이브러리 개발에서는 쉽게 착각하게 됩니다. 내가 테스트한 것은 기능이 아니라 개발 환경일 수 있습니다.
이후에는 확인 기준을 바꿨습니다. 기능을 구현한 뒤에는 단순히 데모 페이지에서 동작하는지만 보지 않고, 실제 빌드 결과물을 기준으로 확인했습니다. ESM과 CJS 엔트리가 의도한 대로 생성됐는지, 외부 의존성이 어떤 형태로 포함되거나 참조되는지, 소비 환경에서 import 방식이 깨지지 않는지를 함께 봤습니다. 이 경험을 통해 라이브러리 개발에서 중요한 기준을 배웠습니다.
- 로컬에서 돌아간다고 끝이 아니다.
- 사용자는 src가 아니라 dist를 사용한다.
- ESM과 CJS를 함께 지원한다면 두 포맷 모두 실제로 실행해봐야 한다.
- 외부 라이브러리의 import 방식은 번들러를 통과하면서 달라질 수 있다.
- 배포형 라이브러리는 기능 구현뿐 아니라 소비 환경까지 책임져야 한다.
웹앱 개발에서는 보통 내가 만든 앱을 내가 정한 환경에서 배포합니다.
하지만 라이브러리는 사용자의 프로젝트 안으로 들어갑니다. 그 순간부터는 내가 통제할 수 없는 번들러, 모듈 시스템, 실행 환경을 만나게 됩니다.
이 문제를 겪고 나서 Colbrush를 볼 때 기준이 조금 달라졌습니다. 색상 변환이 정확한지도 중요하지만, 그 로직이 어떤 포맷으로 배포되고 어떻게 소비되는지도 같은 무게로 봐야 했습니다. 사용자는 Colbrush 내부에서 tsup을 쓰는지, colorjs.io가 ESM인지, CJS 번들이 어떻게 만들어지는지 알 필요가 없습니다. 그저 설치했을 때 자신의 프로젝트에서 동작해야 합니다.
라이브러리 개발자는 그 당연한 기대를 맞추기 위해 빌드 결과물까지 책임져야 한다는 것을 배웠습니다.
9. 마무리
Colbrush를 만들면서 접근성은 “나중에 추가하는 기능”이 아니라 개발 단계에서부터 고려해야 하는 기본 품질이라는 관점을 더 분명하게 갖게 됐습니다. 동시에 라이브러리 개발은 기능 구현만으로 끝나지 않는다는 것도 배웠습니다.
색상 변환 알고리즘을 만드는 것만큼이나 문서화, 배포 포맷, 스타일 충돌, 초기 렌더링, 외부 소비 환경을 함께 설계해야 했습니다. 오픈소스 개발자대회 수상과 다운로드 수 자체도 의미 있었지만, 더 큰 수확은 “다른 개발자가 실제로 사용할 수 있는 구조”를 고민해봤다는 점이었습니다.
Colbrush를 만들기 전에는 접근성 문제를 주로 UI의 문제로만 생각했습니다. 하지만 프로젝트를 진행하면서 접근성은 UI, 상태 관리, 빌드, 배포, 문서화까지 이어지는 개발 경험 전체의 문제라는 것을 알게 됐습니다. 그 점에서 Colbrush는 단순한 대회 프로젝트라기보다, 제가 라이브러리 개발을 실제 사용자 관점에서 바라보게 만든 프로젝트였습니다.
colbrush.sitecolbrushGitHubGitHub - 2025-OSDC/colbrush: Colorblind theme library for colorblind peopleColorblind theme library for colorblind people. Contribute to 2025-OSDC/colbrush development by creating an account on GitHub.Enjoyed this article? Check out more projects and posts on my portfolio.
Explore this project