# 파비콘의 색상이 배경 색상과 동일할 때 캔버스로 대응하기

유사 도메인 검색 기능 중, 검색된 도메인의 파비콘이 노출되는 경우가 존재한다.

그러면서 가끔 파비콘이 정상적으로 로드되지 않는 것처럼 로고가 뜨지 않는 현상이 몇몇보였다.

![Image](https://upload.cafenono.com/image/slashpagePost/20260601/221042_otwCMuh2EQs921Jbwy?q=80&s=1280x180&t=outside&f=webp)

그런데 인프라 수집 단에서 수집하는 서비스의 OS 설정 모드에 따른 파비콘이 다크모드에 대응되는 색상만 수집한다는 것을 알게되었다.

![Image](https://upload.cafenono.com/image/slashpagePost/20260601/221134_ST9dbjCuMLPsRURMDY?q=80&s=1280x180&t=outside&f=webp)

실제로, 다크모드로 전환해서 로고를 확인하게 되면 잘보인다. 사실 로고는 정상적으로 출력되지만 배경색과 파비콘의 로고색이 동일하여 보이지 않는 것처럼 노출되는 현상이었다. 인프라 파트에서는 시스템에 맞는 로고를 모두 가져올 수 있도록 수집 스크립트를 수정하면 되는 일이지만, 작업 시간이 조금 걸리기에 인프라 + 클라이언트의 수정 공수가 모두 들어가는 것보다 클라이언트에서 자체적으로 처리할 수 있는지에 대한 고민을 했다.

## 백그라운드 색상으로 처리해보기

CSS를 이용해 배경 색상을 줌으로 처리하는 것이 제일 단순한 방법이다. 파비콘이 서버에서 제공되면 백그라운드 이미지로 처리하면 무조건 보이게 된다. 그런데 이 방법에서 내가 생각하는 우려점은 아래와 같다.

1. 고정된 백그라운드 색상을 적용하게 되면, 어찌되었든 백그라운드 색상과 동일한 색상의 파비콘이 나오게 되면 똑같이 정사각형의 단색으로 나오게 될 것이다.

2. 투명 배경의 파비콘 아이콘으로 설정한 도메인들의 경우에는 강제로 백그라운드 색상이 적용되어 보이게 될 것이다.

 

이 방법은.. 가장 간단한 방법이긴하지만.. 브랜드 아이콘 디자인을 해치는 것 같아 절레절레.. 했다.

## 캔버스로 파비콘을 읽어 픽셀로 검토해보기

백그라운드 색상으로 처리하는 방법은 빠르게 처리할 수 있다는 장점이 있지만, 그다지 좋은 선택지는 아닌 것 같았다. 그래서 이미지는 Base64 형태로 제공이 되며, 파비콘을 캔버스 엘리먼트의 픽셀 단위로 검토하여 로고가 흰색인지 여부에 대한 판단을 하기로 했다. 만약 로고가 흰색이라면 클래스 네임을 추가해 로고의 색상을 반전시켜주면 되는 일이다.

![Image](https://upload.cafenono.com/image/slashpagePost/20260601/221753_N21xG6P9GNURfHTlyV?q=80&s=1280x180&t=outside&f=webp)

코드는 컴포저블로 재사용 가능하도록 만들었고, 자세한 코드는 아래와 같다.

```javascript
/**
 * base64 data URL 파비콘의 밝기와 내용물 유무를 분석합니다.
 *
 * @param {number} [threshold=240] - 밝기 기준값 (0~255). 이 값 이상이면 색 반전 적용.
 * @returns {{ analyze: (src: string) => Promise<{ isEmpty: boolean, needsInvert: boolean }> }}
 */

export function useFaviconBg(threshold = 240) {
  const cache = new Map();

  /**
   * @param {string} src - data:image/png;base64,... 형식의 URL
   * @returns {Promise<{ isEmpty: boolean, needsInvert: boolean }>}
   */
  const analyze = (src) => {
    if (!src) return Promise.resolve({ isEmpty: true, needsInvert: false });
    if (cache.has(src)) return Promise.resolve(cache.get(src));

    return new Promise((resolve) => {
      const img = new Image();
      img.onload = () => {
        try {
          const canvas = document.createElement('canvas');
          canvas.width = img.width || 16;
          canvas.height = img.height || 16;
          const ctx = canvas.getContext('2d');
          ctx.drawImage(img, 0, 0);

          const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
          const totalPixels = canvas.width * canvas.height;
          let total = 0;
          let count = 0;

          for (let i = 0; i < data.length; i += 4) {
            if (data[i + 3] < 10) continue; // 투명 픽셀 제외
            // perceived luminance
            total += 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
            count++;
          }

          // 전체 픽셀의 2% 미만이 불투명하면 빈 이미지로 간주
          const isEmpty = count < totalPixels * 0.02;
          const needsInvert = !isEmpty && count > 0 && total / count >= threshold;

          const result = { isEmpty, needsInvert };
          cache.set(src, result);
          resolve(result);
        } catch {
          resolve({ isEmpty: false, needsInvert: false });
        }
      };
      img.onerror = () => resolve({ isEmpty: true, needsInvert: false });
      img.src = src;
    });
  };

  return { analyze };
}
```

해당 코드들은 차근차근 살펴보면 아래와 같은 흐름으로 진행이 된다.

## 캔버스의 픽셀 분석 흐름 확인해보기

`analyze(src)` 코드를 통해 해당 `src` 이미지가 빈 이미지인가, 흰색 이미지인가를 판단한다.

### 전체 흐름

전체적인 컴포저블의 흐름은 아래와 같이 흘러간다.

```javascript
파비콘 src 입력
      │
      ▼
┌─────────────┐    없음
│  src 있음?  │ ─────────▶  { isEmpty: true, needsInvert: false }
└─────────────┘
      │ 있음
      ▼
┌─────────────┐    있음
│ 캐시 있음?  │ ─────────▶  캐시된 결과 즉시 반환
└─────────────┘
      │ 없음
      ▼
  이미지 로드
  Canvas에 그리기
  픽셀 데이터 추출
      │
      ▼
  픽셀 순회 (4개씩: R G B A)
      │
      ├─ Alpha < 10  →  투명 픽셀 → 건너뜀
      │
      └─ Alpha >= 10 →  불투명 픽셀 → 밝기 계산 후 누적
      │
      ▼
  isEmpty 판단
  needsInvert 판단
      │
      ▼
  캐시 저장 후 반환
```

### useFaviconBg

파비콘 이미지를 분석해서 두 가지를 판단한다.

```javascript
analyze(src)
  │
  ├─ 빈 이미지인가? (isEmpty)
  │
  └─ 흰색 이미지인가? (needsInvert)
```

### isEmpty 플래그 판단

isEmpty 플래그는 내부적으로 아래와 같은 로직을 통해 판단한다.

```javascript
전체 픽셀 수 (width × height)
        │
        × 0.02  (2%)
        │
        ▼
   기준 픽셀 수

불투명 픽셀 수  <  기준 픽셀 수
        │
        ▼
    isEmpty = true  →  el.hidden = true (화면에서 숨김)

예시 (16×16 파비콘)

전체 픽셀: 16 × 16 = 256개
기준:      256 × 0.02 = 5.12개

불투명 픽셀 0개  →  isEmpty ✓  (완전 투명 이미지)
불투명 픽셀 3개  →  isEmpty ✓  (노이즈 수준)
불투명 픽셀 6개  →  isEmpty ✗  (정상 이미지로 간주)
```

### needInvert 판단 플로우

흰색을 감지하는 플로우이며, 픽셀 밝기는 RGB를 그대로 평균내지 않고, 사람 눈의 민감도 기준으로 가중치를 적용한다.

```javascript
밝기 = 0.299 × R  +  0.587 × G  +  0.114 × B
         빨강 29%      초록 59%      파랑 11%
```

초록에 가중치가 높은 이유는 사람 눈이 초록빛에 가장 민감하기 때문이다.

```javascript
모든 불투명 픽셀의 밝기 합계
        ÷
    불투명 픽셀 수
        │
        ▼
    평균 밝기

평균 밝기  >=  240 (threshold)
        │
        ▼
  needsInvert = true  →  filter: invert(1) 적용

밝기 기준 예시
  0 ──────────────────────────────────── 255
  │                          │           │
 검정                       240         흰색
                              ▲
                         여기 이상이면
                         반전 적용
```

### 컴포넌트에서의 최종 동작

```javascript

```

## 매 리스트마다 캔버스를 그리는데, 성능상 이슈는 없을까?

현재 리스트에서는 파비콘이 스크래핑되는 경우도 있고, 안되는 경우가 있어 100개의 리스트를 로드했을 때 100개 모두 파비콘을 가져오는 형태는 아니다. 그렇기 때문에 실제로 적용을 했을 때 성능 상의 이슈는 없었으나 100개의 파비콘이 존재하는 리스트를 로드할 때는 조금의 버벅임이 있을 수 있다.

그래서 캐싱 기능을 넣었다. 내부 로직에서 캐싱 처리되기 때문에 동일한 파비콘이 여러 행에 반복 출현해도 Canvas 분석은 딱 1번만 실행된다. 하지만 캐싱 기능이 올바른 해답은 아니다. 라이트, 다크모드의 파비콘이 스크래핑이 정상적으로 될때까지 사용자에게 파비콘이 가장 자연스럽게 노출될 수 있도록 할 수 있는 최선의 방법이라고 생각했다. 스크래핑 대응이 완료되면 해당 로직은 롤백해서 성능 상의 이슈는 다시 고쳐놓긴해야겠다,,

```javascript
첫 번째 행:  analyze("data:image/png;base64,ABC...")  →  Canvas 분석 실행 → 캐시 저장
두 번째 행:  analyze("data:image/png;base64,ABC...")  →  캐시에서 즉시 반환 (분석 생략)
세 번째 행:  analyze("data:image/png;base64,ABC...")  →  캐시에서 즉시 반환 (분석 생략)
```

For the site tree, see the [root Markdown](https://slashpage.com/timmy.md).
