프론트엔드 개발자라면 한 번쯤은 이런 상황을 겪어보셨을 겁니다. “Chrome에서는 분명히 잘 되는데, Safari에서만 안 되네요?” 네, 오늘도 어김없이 Safari입니다.
요즘 회사 프로젝트 QA 기간 막바지라 크로스 브라우징 테스트를 열심히 하고 있는데요, 역시나 Safari 이슈가 저를 반겨주더라구요. 아직 해결하지 못한 몇몇 이슈들이 남아있어서 요즘 글쓸 소재가 넘쳐나는 호사(?)를 누리고 있습니다.
그라데이션과 트랜지션을 조합하면 세련된 느낌을 얻을 수 있다
이번에 마주한 이슈는 Safari 18 이하 버전에서 CSS currentColor 값을 이용해 구현된 그라데이션 트랜지션을 적용했음에도 불구하고 제대로 적용되지 않는 현상입니다. 원인을 파악하고 나니 브라우저 엔진 자체의 렌더링 최적화 과정에서 발생한 버그였고, 이를 해결하는 방법도 찾아서 공유해보려고 합니다.
이번 포스트를 통해 CSS 그라데이션에 트랜지션 효과를 적용하는 방법이 궁금하신 분들, 그리고 Safari에서 해당 트랜지션이 동작하지 않는 이슈를 겪고 계신 분들에게 도움이 되길 바랍니다.
TL;DR
- CSS 그라데이션 배경은
background-image속성에linear-gradient()등의 CSS 함수를 사용하여 구현함background-image는 기본적으로 트랜지션이 적용되지 않는 불연속(discrete) 속성이지만,linear-gradient()내부에currentColor를 사용하면color속성의 트랜지션에 따라 그라데이션 색상도 자연스럽게 전환됨- 그러나 특정 Safari 버전에서는
color값이 변경되어도background-image내부의currentColor가 업데이트되지 않는 버그가 있음- 이는 Safari(WebKit)가
background-image내 CSS 함수로 참조된currentColor변경을 감지하지 못해 리페인트가 트리거되지 않기 때문- 이를 해결하기 위해서는
background-color: currentColor와 같은 리페인트를 유도하는 속성을 추가하는 방법을 고려할 수 있음
그라데이션 트랜지션 구현하기
그라데이션 트랜지션의 동작 예시, 1번은 호버 시 트랜지션, 2번은 버튼 클릭 시 트랜지션
우선 그라데이션 배경의 색상이 자연스럽게 변하는 정상적인 상황을 재현한 예시를 제작해보았습니다.
위 영상에는 그라데이션 배경이 적용된 <div>가 2개 보이는데요, 첫 번째는 호버 시 트랜지션, 두 번째는 버튼 클릭 시 트랜지션을 적용한 예시를 볼 수 있습니다. 그라데이션이 예쁘게 잘 전환되네요!
이러한 그라데이션을 구현하기 위해서는 어떻게 CSS를 작성해야 할까요? 만약 아래와 같은 코드를 작성하면 의도한 대로 트랜지션 효과가 적용될까요?
.box {
/* 초기 그라데이션 배경 */
background-image: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
/* 트랜지션 효과 */
transition: background-image 0.5s ease;
}
.box:hover {
/* 호버 시 그라데이션 배경 */
background-image: linear-gradient(135deg, #9b59b6 0%, #4ecdc4 100%);
}의도한 대로 나오지 않는다
위 코드를 그대로 적용하면 트랜지션 효과가 전혀 적용되지 않고 색상이 뚝 끊기듯이 변하는 것을 볼 수 있습니다. 그 이유는 바로 background-image 속성은 트랜지션을 적용할 수 없는 불연속(discrete) 속성이기 때문입니다. 그래서 일반적으로 그라데이션 배경에 트랜지션을 적용하기 위해서는 약간의 트릭을 사용해야 합니다.
투명도 조절으로 구현하기
가장 일반적인 방법은 ::before 혹은 ::after 를 사용해 전환할 그라데이션 배경을 만들고, 가상 요소의 opacity 값을 조절하는 것입니다. 구글에 검색해보았을 때 레퍼런스도 가장 많고 간단한 방법입니다.
.box {
position: relative;
/* 초기 그라데이션 배경 */
background-image: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
}
.box::after {
/* box를 덮는 가상 요소 */
position: absolute;
content: '';
top: 0;
right: 0;
bottom: 0;
left: 0;
/* 호버 시 그라데이션 배경 */
background-image: linear-gradient(135deg, #9b59b6 0%, #4ecdc4 100%);
/* box를 덮는 가상 요소의 투명도를 조절하여 그라데이션 배경을 표시 */
transition: opacity 0.5s ease;
/* 초기 가상 요소의 opacity는 0 */
opacity: 0;
}
.box:hover::after {
/* 호버 시 가상 요소의 opacity를 1로 변경 */
opacity: 1;
}의도한 대로 나온다!
이 방법은 단순히 레이어의 투명도 조절을 통해 그라데이션 배경을 전환하는 방법이기 때문에 대부분의 브라우저에서 잘 동작합니다. 하지만 이와 같은 방법을 사용하기 어려운 경우에는 다른 방법으로도 트랜지션을 구현할 수 있는데, 바로 currentColor를 활용하는 방법입니다.
currentColor로 구현하기
currentColor는 CSS에서 현재 요소의 color 속성 값을 참조하는 키워드입니다. 이를 통해 다른 속성이 color 속성 값에 따라 자동으로 변경될 수 있게 하는데요, 즉, color: red로 설정하면 currentColor도 red가 되는 거죠.
이를 활용하면 background-image 속성 내부에서 linear-gradient가 currentColor를 참조하게 하는 경우, color 속성을 변경하는 방식으로 그라데이션의 트랜지션을 쉽게 구현할 수 있습니다.
.box {
/* 초기 color 값 */
color: #ff6b6b;
/* color에 transition 적용 */
transition: color 0.5s ease;
/* currentColor를 사용한 gradient, currentColor는 현재 요소의 color 속성 값을 참조 */
background-image: linear-gradient(135deg, currentColor 0%, #4ecdc4 100%);
}
.box:hover {
/* 호버 시 color 값 변경 */
color: #9b59b6;
}위 방법도 잘 된다! 무엇보다 코드가 매우 간결함
이 키워드를 활용하면 color 속성 하나만 변경해도 연관된 모든 스타일이 함께 바뀌기 때문에 유지보수가 편리합니다. 또한, color 속성은 인라인 스타일을 통해 외부에서 쉽게 주입할 수도 있기 때문에, 그라데이션 구성 요소가 동적이어야 하는 경우의 트랜지션을 더욱 쉽게 구현할 수 있습니다.
Safari의 렌더링 최적화 문제
좌측은 Safari 18, 우측은 Safari 26. 똑같은 코드인데 왜 여기서만 안 될까?
Chrome에서 테스트했을 때는 그라데이션 트랜지션이 의도한 대로 완벽하게 동작했습니다. 그런데 Safari에서 확인해보니… 색상이 아예 변하지를 않더라구요. 몇 번의 삽질(?)과 테스트를 통해 정확하게 Safari 18 이하의 버전에서 해당 코드가 동작하지 않는 것으로 범위를 좁혀낼 수 있었습니다.
Chrome과 Safari 26 버전에서는 마우스를 올리면 빨간색에서 파란색으로 0.5초에 걸쳐 자연스럽게 전환됩니다. 하지만 Safari 18 이하에서는 색상이 뚝 끊기듯이 변하거나 아예 색상 변경이 일어나지 않는 경우도 있었습니다.
정확한 원인을 파악하기 위해, Safari 브라우저의 개발자 도구를 통해 렌더링 타임라인을 살펴보았습니다.
우선 정상적으로 동작하고 있던 Safari 26에서의 타임라인을 보면, 트랜지션이 발생할 때 브라우저의 페인팅 단계가 매 프레임마다 잘 발생하는 것을 볼 수 있었습니다.
문제 케이스 1: 색상 변경이 아예 발생하지 않는 상황
반면 문제가 되는 케이스에서는 iOS Safari와 PC Safari에서 서로 다른 증상을 보였습니다. iOS Safari에서는 색상 변경이 아예 발생하지 않는 상황을 마주할 수 있었습니다. 실제로 페인트 단계가 전혀 발생하지 않은 것을 확인할 수 있었습니다.
문제 케이스 2: 트랜지션 없이 그라데이션 변경이 뚝 끊겨 발생하는 상황
반면 PC Safari에서는 트랜지션 없이 그라데이션 변경이 뚝 끊겨 발생하는 상황이 있었습니다. ‘스타일 무효화됨’ 과 ‘스타일 재검토됨’ 이 반복될 뿐, 실제 페인트 단계는 트랜지션의 마지막 프레임에서만 발생한 것을 확인할 수 있었습니다.
이를 통해 문제의 원인을 확실히 파악할 수 있었습니다.
“구 버전 Safari 브라우저의 중요 렌더링 경로에서 리페인팅 단계가 누락되어 발생한 문제구나!”
왜 이런 현상이 발생하는 걸까요? Safari 렌더링 엔진의 내부 동작이라 정확하게 알 순 없었지만, 다음과 같이 추론해보았습니다.
일반적으로 color 속성이 변경되면 브라우저는 해당 요소와 연관된 스타일을 다시 계산하고 화면을 갱신(repaint)합니다. 그런데 Safari에서는 linear-gradient() 내부의 currentColor에 대해서는 이 과정이 정상적으로 동작하지 않았습니다.
색상 변경이 아예 발생하지 않는 상황에 대해서는, 아무래도 Safari에서 linear-gradient() 함수가 처음 렌더링될 때 currentColor를 실제 색상 값으로 해석한 뒤, 이를 정적 값으로 캐싱하기 때문으로 보입니다. 이후 color 속성이 변경되어 currentColor의 참조 값이 바뀌더라도, linear-gradient() 입장에서는 이미 캐싱된 값을 사용하기 때문에 변경을 감지하지 못하는 것이죠.
트랜지션 없이 그라데이션 변경이 뚝 끊겨 발생하는 상황에 대해서는, Safari 개발자 도구의 프레임 별 메시지를 통해 그 단서를 추측해볼 수 있었습니다. currentColor의 값이 바뀌는 순간 기존의 리페인팅을 취소하고 새로 들어온 값을 기준으로 그라데이션을 계산하려고 하는데, 이것이 매 프레임마다 발생해서 연쇄적으로 취소가 이어진 현상으로 보입니다.
해결 방법
자, 이제 해결책을 알아볼 차례입니다.
강제 리페인트 유도
가장 간단한 해결책은 리페인트를 강제로 유도하는 것입니다. 페인트가 일어나지 않아서 생긴 문제니까 페인트를 일으키는 속성을 추가하면 되겠죠.
결과물에 영향을 주지 않으면서 강제 리페인트를 유도하기 위해서는 background-color: currentColor를 함께 추가하는 것을 고려할 수 있습니다.
.box {
color: #ff6b6b;
transition: color 0.5s ease;
/* background-color 속성에도 currentColor 속성을 추가하여 강제 리페인트를 유도 */
background-color: currentColor;
background-image: linear-gradient(135deg, currentColor 0%, #4ecdc4 100%);
}강제 리페인트 유도
이게 왜 동작할까요? background-color 속성의 currentColor는 Safari에서도 정상적으로 리페인트를 트리거합니다. 그리고 background-image가 background-color 위에 렌더링되기 때문에, 추가한 background-color는 시각적으로 보이지 않습니다.
background-image 렌더 시 background-color를 같이 설정해두는 것이 좋다는 권장 사항도 있다
결과적으로 color가 변경되면 background-color의 변경으로 인해 리페인트가 발생하고, 이 과정에서 background-image의 그라데이션도 함께 다시 그려지는 거죠.
@property로 CSS 변수 애니메이션
저는 위의 방법으로 문제를 해결했는데, 글을 작성하면서 새로운 방법으로 문제를 해결하는 법도 알게 되어 이를 소개해보고자 합니다. 바로 CSS의 @property 규칙을 사용하는 것입니다.
/* gradient-color 변수 정의 */
@property --gradient-color {
syntax: '<color>';
initial-value: #ff6b6b;
inherits: false;
}
.box {
--gradient-color: #ff6b6b;
/* --gradient-color 변수에 transition 적용 */
transition: --gradient-color 0.5s ease;
background-image: linear-gradient(
135deg,
var(--gradient-color) 0%,
#4ecdc4 100%
);
}
.box:hover {
--gradient-color: #9b59b6;
}
@property로 CSS 변수 애니메이션
이 방법이 동작하는 이유는 @property로 정의된 변수는 브라우저가 명시적으로 추적하는 대상이 되기 때문입니다. currentColor 처럼 다른 속성으로부터 참조되는 값이 아니라, 직접 변경되는 값으로 취급됩니다. 이로 인해 Safari에서도 @property 변수의 변경을 정상적으로 감지하고 올바르게 리페인트를 트리거합니다.
마무리
어쩌다 보니 새해 첫 기술 포스트도 Safari와 함께…
오늘은 이렇게 해서 구 버전 Safari의 렌더링 최적화 과정에서 발생한 버그를 해결하는 과정에 대해 다루어보았습니다. 문제의 해결 방법은 간단했지만, 브라우저의 중요 렌더링 경로에 대한 이해가 없었더라면 아마 쉽게 해결하지 못했을 것 같아요. 새삼 기본기(?)를 잘 닦아두는 게 중요하다는 생각이 듭니다.
혹시 비슷한 문제를 겪고 계신 분들께 이 글이 도움이 되길 바랍니다.

