STUDY/[ React ]

CSS opacity와 brightness 비교 + filter 속성

Lim임 2025. 11. 20. 20:47

어쩌다가?

장기 프로젝트의 프론트를 맡던 중

비활성화 css가 제대로 안들어가는 것을 보고 ai에게 문의한 결과

opacity를 filter: brightness 로 변경하라는 조언을 듣고 해결했다

엥 왜요?

한 줄 요약

opacity 합성 레이어 생성해서 border가 어긋날 수 있지만, brightness 색상만 변경해서픽셀 위치가 완벽하게 유지됩니다!

CSS Filter 속성 학습 문서

1. opacity vs brightness 비교

opacity

.element {
  opacity: 0.7; /* 0 (완전 투명) ~ 1 (완전 불투명) */
}

특징:

  • 전체 요소의 투명도를 조절
  • 요소와 그 모든 자식 요소에 영향
  • 새로운 합성 레이어(compositing layer) 생성
  • 안티앨리어싱(anti-aliasing) 방식이 변경됨
  • 서브픽셀 렌더링(sub-pixel rendering) 영향

렌더링 과정:

  1. 요소를 별도 레이어로 분리
  2. 해당 레이어 전체에 투명도 적용
  3. 다른 레이어와 합성(blend)
  4. 이 과정에서 픽셀 위치가 미세하게 이동할 수 있음

사용 예시:

/* 전체 요소를 반투명하게 */
.modal-overlay {
  opacity: 0.8;
  background: black;
}

/* 요소 전체를 숨기기/보이기 애니메이션 */
.fade-out {
  opacity: 0;
  transition: opacity 0.3s;
}

brightness

.element {
  filter: brightness(70%); /* 0% (완전 검정) ~ 100% (원본) ~ 200%+ (더 밝게) */
}

특징:

  • 요소의 색상 값만 조절
  • 픽셀 위치는 변경하지 않음
  • 레이어 분리 없이 색상 필터만 적용
  • 픽셀 퍼펙트(pixel-perfect) 정렬 유지
  • RGB 값에만 영향 (구조는 그대로)

렌더링 과정:

  1. 각 픽셀의 RGB 값 계산
  2. 밝기 비율만큼 RGB 값 조정
  3. 픽셀 위치는 원본과 동일하게 유지

사용 예시:

/* 비활성화된 요소 어둡게 */
.disabled {
  filter: brightness(70%);
  pointer-events: none;
}

/* 호버 시 밝게 */
.button:hover {
  filter: brightness(110%);
}

2. Border 정렬 문제의 원인

문제 상황

/* ❌ 문제가 있던 코드 */
.piece-position.unable {
  opacity: 0.7;
}

왜 border가 안 맞았을까?

  1. 합성 레이어 생성
    • opacity는 요소를 별도 레이어로 분리
    • 이 레이어는 GPU에서 별도로 처리됨
  2. 서브픽셀 렌더링 변화
    • 레이어 합성 과정에서 안티앨리어싱 재계산
    • 1px border가 0.8px이나 1.2px처럼 보일 수 있음
    • 픽셀 경계가 흐릿하거나 어긋나 보임
  3. 시각적 효과
  4. 원본: ┌─────┐ │ │ (선명한 1px border) └─────┘ opacity: ┌═════┐ ║ ║ (약간 두껍거나 흐릿한 border) └═════┘ opacity: 0.7 ┌─────────┐ │ ░░░░░░░ │ ← 전체가 투명해져서 경계가 흐릿 │ ░PIECE░ │ 픽셀이 미세하게 어긋남 │ ░░░░░░░ │ └─────────┘ brightness(70%) ┌─────────┐ │ ████████│ ← 경계는 선명하고 정확 │ █PIECE█ │ 색만 어둡게 │ ████████│ └─────────┘

해결 방법

/* ✅ 해결된 코드 */
.piece-position.unable {
  filter: brightness(70%);
  pointer-events: none;
}

왜 brightness는 괜찮을까?

  1. 픽셀 위치 유지
    • 합성 레이어 생성 없음
    • 원본 픽셀 위치 그대로 유지
  2. 색상만 변경
  3. 원본 border: rgb(139, 69, 19) → brightness(70%) → rgb(97, 48, 13) 위치: (10px, 10px) → (10px, 10px) (동일) 두께: 1px → 1px (동일)
  4. 시각적 일관성
    • border의 위치, 두께, 선명도 모두 동일
    • 색상만 어두워짐

3. CSS Filter 속성 전체 목록

3.1 brightness (밝기)

filter: brightness(50%);   /* 어둡게 */
filter: brightness(100%);  /* 원본 */
filter: brightness(150%);  /* 밝게 */
  • 범위: 0% ~ ∞
  • 기본값: 100%
  • 효과: RGB 값을 곱셈으로 조정

3.2 contrast (대비)

filter: contrast(50%);   /* 대비 낮게 (회색조에 가깝게) */
filter: contrast(100%);  /* 원본 */
filter: contrast(200%);  /* 대비 높게 (색상 강조) */
  • 범위: 0% ~ ∞
  • 기본값: 100%
  • 효과: 중간 회색(50%)을 기준으로 어두운 색은 더 어둡게, 밝은 색은 더 밝게

3.3 grayscale (회색조)

filter: grayscale(0%);    /* 원본 색상 */
filter: grayscale(50%);   /* 반만 회색 */
filter: grayscale(100%);  /* 완전 회색 */
  • 범위: 0% ~ 100%
  • 기본값: 0%
  • 효과: 모든 색상을 회색조로 변환

사용 예시:

/* 비활성화된 이미지 */
.image-disabled {
  filter: grayscale(100%);
}

/* 호버 시 색상 복원 */
.image-disabled:hover {
  filter: grayscale(0%);
  transition: filter 0.3s;
}

3.4 sepia (세피아 톤)

filter: sepia(0%);    /* 원본 */
filter: sepia(50%);   /* 반만 세피아 */
filter: sepia(100%);  /* 완전 세피아 (갈색 빛) */
  • 범위: 0% ~ 100%
  • 기본값: 0%
  • 효과: 오래된 사진 같은 갈색 톤

3.5 saturate (채도)

filter: saturate(0%);    /* 무채색 (grayscale과 유사) */
filter: saturate(100%);  /* 원본 */
filter: saturate(200%);  /* 채도 2배 (선명한 색상) */
  • 범위: 0% ~ ∞
  • 기본값: 100%
  • 효과: 색상의 선명도 조절

3.6 hue-rotate (색조 회전)

filter: hue-rotate(0deg);    /* 원본 */
filter: hue-rotate(90deg);   /* 90도 회전 (빨강→노랑) */
filter: hue-rotate(180deg);  /* 180도 회전 (빨강→청록) */
filter: hue-rotate(360deg);  /* 한 바퀴 (원본과 동일) */
  • 범위: 0deg ~ 360deg
  • 기본값: 0deg
  • 효과: HSL 색상환을 기준으로 색상 변경

색상 변화:

  • 0deg → 원본
  • 90deg → 빨강→노랑, 초록→청록, 파랑→보라
  • 180deg → 색상 반전 (보색)
  • 270deg → 빨강→청록, 초록→보라, 파랑→노랑

3.7 invert (색상 반전)

filter: invert(0%);    /* 원본 */
filter: invert(50%);   /* 중간 회색 */
filter: invert(100%);  /* 완전 반전 (네거티브) */
  • 범위: 0% ~ 100%
  • 기본값: 0%
  • 효과: 색상 네거티브 효과

변화 예시:

  • 흰색(#ffffff) → 검정(#000000)
  • 빨강(#ff0000) → 청록(#00ffff)
  • 파랑(#0000ff) → 노랑(#ffff00)

3.8 blur (흐림 효과)

filter: blur(0px);    /* 선명 */
filter: blur(5px);    /* 약간 흐림 */
filter: blur(10px);   /* 많이 흐림 */
  • 범위: 0px ~ ∞
  • 기본값: 0px
  • 효과: 가우시안 블러 (Gaussian blur)

사용 예시:

/* 배경 블러 */
.backdrop {
  filter: blur(8px);
}

/* 로딩 중 콘텐츠 */
.loading {
  filter: blur(3px);
  pointer-events: none;
}

3.9 drop-shadow (그림자)

filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.5));
/* drop-shadow(offset-x offset-y blur-radius color) */
  • 매개변수:
    • offset-x: 수평 거리 (양수=오른쪽, 음수=왼쪽)
    • offset-y: 수직 거리 (양수=아래, 음수=위)
    • blur-radius: 흐림 정도 (선택사항, 기본 0)
    • color: 그림자 색상 (선택사항)

box-shadow와 차이점:

/* box-shadow: 박스 형태 그림자 */
.box {
  box-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}

/* drop-shadow: 실제 형태를 따라가는 그림자 */
.shape {
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
  filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.5));
}

중요: clip-path를 사용한 요소는 box-shadow가 작동하지 않으므로 drop-shadow를 사용해야 함!


3.10 opacity (투명도) - filter 버전

filter: opacity(50%);   /* 반투명 */
filter: opacity(100%);  /* 불투명 */
  • 범위: 0% ~ 100%
  • 기본값: 100%
  • 차이점: opacity 속성과 거의 동일하지만 filter 체인에 포함 가능

4. Filter 조합 사용

여러 필터 동시 적용

.element {
  filter: brightness(110%) contrast(120%) saturate(130%);
}

순서 중요:

/* 순서에 따라 결과가 다름 */
.example1 {
  filter: grayscale(100%) sepia(100%);
  /* 1. 회색으로 변환 → 2. 세피아 톤 적용 */
}

.example2 {
  filter: sepia(100%) grayscale(100%);
  /* 1. 세피아 톤 적용 → 2. 회색으로 변환 (결국 회색) */
}

실용적인 조합 예시

비활성화 효과

.disabled {
  filter: brightness(70%) grayscale(50%);
  /* 어둡게 + 색상 제거 */
}

빈티지 사진 효과

.vintage {
  filter: sepia(60%) contrast(110%) brightness(90%);
}

네온 효과

.neon {
  filter: brightness(150%) saturate(200%) contrast(120%);
}

다크모드 이미지

.dark-mode img {
  filter: invert(100%) hue-rotate(180deg);
  /* 색상 반전 + 색조 복원 */
}

5. 성능 고려사항

하드웨어 가속

다음 필터는 GPU 가속이 가능:

  • blur()
  • drop-shadow()
  • opacity()

나머지는 CPU에서 처리됨.

성능 팁

/* ❌ 나쁜 예: 매 프레임마다 재계산 */
.animated {
  animation: complexFilter 1s infinite;
}

@keyframes complexFilter {
  0% { filter: brightness(100%) contrast(100%) saturate(100%); }
  50% { filter: brightness(120%) contrast(110%) saturate(120%); }
  100% { filter: brightness(100%) contrast(100%) saturate(100%); }
}

/* ✅ 좋은 예: 단순한 필터 사용 */
.animated {
  animation: simpleFilter 1s infinite;
}

@keyframes simpleFilter {
  0% { filter: brightness(100%); }
  50% { filter: brightness(120%); }
  100% { filter: brightness(100%); }
}

6. 브라우저 호환성

대부분의 현대 브라우저에서 지원:

  • Chrome/Edge: 18+
  • Firefox: 35+
  • Safari: 9.1+
  • iOS Safari: 9.3+

주의사항:

  • IE 11 이하: 지원 안 함
  • 오래된 브라우저를 지원해야 한다면 fallback 제공:
.element {
  /* 구형 브라우저용 fallback */
  opacity: 0.7;

  /* 현대 브라우저용 */
  filter: brightness(70%);
}

/* 또는 @supports 사용 */
@supports (filter: brightness(70%)) {
  .element {
    filter: brightness(70%);
    opacity: 1; /* filter 지원 시 opacity 무효화 */
  }
}

7. 실전 활용 예시

장기 프로젝트에서의 활용

비활성화된 기물

.piece-position.unable {
  filter: brightness(70%);
  pointer-events: none;
}

이유: 픽셀 정렬 유지하면서 어둡게 표현

선택된 기물 강조

.piece-position.selected {
  filter: brightness(120%) drop-shadow(0 0 8px rgba(255,255,0,0.6));
}

효과: 밝게 + 노란 그림자로 강조

호버 효과

.piece:hover {
  filter: brightness(110%);
  transition: filter 0.2s;
}

효과: 부드러운 밝기 변화

적 기물 구분

.piece.enemy {
  filter: saturate(80%) brightness(95%);
}

효과: 약간 덜 선명하고 어둡게


요약

속성 opacity filter: brightness()
적용 대상 요소 전체 (자식 포함) 요소의 색상만
레이어 생성 ✅ 생성 ❌ 없음
픽셀 위치 미세하게 이동 가능 완전히 유지
border 정렬 어긋날 수 있음 정확히 유지
성능 약간 느림 (합성) 빠름
사용 용도 투명도 조절, 페이드 효과 밝기 조절, 비활성화

결론: Border나 픽셀 정렬이 중요한 경우 filter: brightness()를 사용하고, 진짜 투명도가 필요한 경우에만 opacity를 사용하자!