Table of contents
Open Table of contents
React + Tailwind: transform과 overflow-hidden의 충돌 문제 해결기
🎯 문제 발견
웹 애플리케이션에서 다운로드 버튼을 구현하던 중 이상한 현상을 발견했습니다.
처음 페이지를 로드했을 때는 버튼이 완벽하게 작동했습니다. 둥근 모서리(rounded-full)가 예쁘게 렌더링되고, 호버 시 그라데이션 효과도 버튼의 경계를 벗어나지 않았죠.
하지만 다운로드 버튼을 클릭한 후, 다시 호버를 하면 왼쪽에 희미한 하얀색 네모가 둥근 모서리를 뚫고 나오는 기괴한 현상이 발생했습니다.
// 문제가 있는 코드const getButtonStyles = (state: DownloadState) => { const baseStyles = ` relative overflow-hidden h-16 px-12 text-lg font-semibold border-0 rounded-full transform transition-all duration-500 ease-in-out // 🚨 문제의 시작 hover:scale-105 hover:shadow-2xl hover:shadow-purple-500/25 disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none `;
return baseStyles;};// 버튼 내부의 호버 효과<div className="absolute inset-0 rounded-full bg-gradient-to-r from-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>🔍 디버깅 과정
1단계: 첫 번째 시도 - “rounded-full을 빼먹었나?”
가장 먼저 의심한 것은 단순한 실수였습니다. 혹시 호버 효과 div에 rounded-full을 빼먹은 건 아닐까?
// 시도 1: rounded-full 추가<div className="absolute inset-0 rounded-full bg-gradient-to-r ..."></div>✅ 결과: 초기 로드 시에는 문제 해결!
❌ 하지만: 다운로드 클릭 후 여전히 같은 증상 발생
2단계: “overflow-hidden이 제대로 작동하나?”
다음으로 의심한 것은 overflow-hidden이 제대로 적용되지 않는다는 것이었습니다.
// 시도 2: overflow-hidden 명시적으로 추가className={`${getButtonStyles(state)} overflow-hidden group`}✅ 결과: 여전히 초기에는 정상
❌ 하지만: 상태 변경 후 동일한 문제
3단계: “왜 다운로드 후에만 발생하지?”
여기서 중요한 힌트를 얻었습니다. 왜 초기에는 괜찮은데, 상태가 변경된 후에만 문제가 발생할까?
React DevTools로 렌더링을 추적해보니, 상태가 변경될 때마다 버튼이 재렌더링되고 있었습니다. 그런데 뭔가 CSS 적용 순서나 방식이 달라지는 것 같았습니다.
💡 문제의 근본 원인
여러 실험과 MDN 문서를 뒤진 끝에, 문제의 원인을 찾았습니다.
원인 1: CSS Transform과 Stacking Context
Tailwind의 transform 유틸리티 클래스는 CSS transform: translateX(0) translateY(0) rotate(0) skewX(0) skewY(0) scaleX(1) scaleY(1)를 적용합니다.
핵심은 이것입니다: CSS transform 속성은 새로운 stacking context를 생성합니다.
🔹 Stacking Context란?요소들의 z축 순서를 결정하는 독립적인 레이어입니다.새로운 stacking context가 생성되면, 그 안의 자식 요소들은외부의 overflow-hidden 규칙과 다르게 작동할 수 있습니다.원인 2: 재렌더링 시 Transform 재적용
const stateStyles = { idle: "bg-purple-600 hover:bg-purple-700", downloading: "bg-purple-600", completed: "bg-green-600", error: "bg-red-600",};상태가 변경되면:
- React가 버튼을 재렌더링
transform클래스가 제거되고 다시 적용됨- Stacking context가 재생성
- 이 과정에서
overflow-hidden과의 관계가 깨짐 - 내부 absolute 요소들이 경계를 무시하기 시작
원인 3: Scale과 Overflow의 미묘한 관계
hover:scale-105 // 버튼을 1.05배 확대scale(1.05)는 요소를 중심으로 확대하는데, 이때:
- 버튼 자체는 확대되지만
- 내부의
absolute요소들도 함께 확대됨 overflow-hidden이 제대로 작동하지 않으면- 확대된 요소가 경계를 벗어남
✅ 해결 방법
해결책: transform 클래스 제거
놀랍게도 해결책은 간단했습니다. 불필요한 transform 클래스를 제거하는 것이었습니다.
// 수정 전const baseStyles = ` relative overflow-hidden h-16 pl-8 text-lg font-semibold border-0 rounded-full transform transition-all duration-500 ease-in-out // ❌ 제거! hover:scale-105 hover:shadow-2xl hover:shadow-purple-500/25 disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none`;// 수정 후const baseStyles = ` relative overflow-hidden h-16 pl-8 text-lg font-semibold border-0 rounded-full transition-all duration-500 ease-in-out // ✅ transform 제거 hover:scale-105 hover:shadow-2xl hover:shadow-purple-500/25 disabled:opacity-70 disabled:cursor-not-allowed disabled:hover:scale-100`;왜 이렇게 하면 해결될까?
-
transform클래스 불필요:hover:scale-105는 이미 자체적으로 transform을 적용합니다. 기본transform클래스는 중복이었습니다. -
Stacking Context 단순화:
transform클래스를 제거하면 초기 상태에서 불필요한 stacking context가 생성되지 않습니다. -
disabled 상태 개선:
disabled:transform-none대신disabled:hover:scale-100을 사용하여 disabled 상태에서도 transform이 일관되게 작동합니다.
📊 Before & After
Before (문제 있는 코드)
// ❌ 초기: 정상 → 상태 변경 후: 깨짐transform transition-allhover:scale-105disabled:transform-none렌더링 사이클:
idle → transform 적용 → stacking context 생성 → overflow 정상 ↓ (다운로드 클릭)downloading → transform 재적용 → stacking context 재생성 → overflow 깨짐! ↓ (완료)completed → transform 재적용 → overflow 여전히 깨짐After (수정된 코드)
// ✅ 항상 정상 작동transition-allhover:scale-105disabled:hover:scale-100렌더링 사이클:
idle → stacking context 없음 → overflow 정상 ↓ (다운로드 클릭)downloading → stacking context 없음 → overflow 정상 ↓ (완료)completed → stacking context 없음 → overflow 정상🎓 배운 점
1. Tailwind의 transform 클래스는 신중하게 사용하자
transform 유틸리티는 편리하지만, 필요하지 않은 곳에 사용하면 예상치 못한 부작용이 있습니다.
// ❌ 나쁜 예className="transform hover:scale-105" // transform 중복!
// ✅ 좋은 예className="hover:scale-105" // scale이 이미 transform을 포함2. CSS Stacking Context를 이해하자
다음 속성들은 새로운 stacking context를 생성합니다:
transform(translateX(0) 포함)opacity(1 미만)position: fixed또는position: stickyz-index(position이 static이 아닐 때)filter,backdrop-filterwill-change
3. 상태 변경 시의 렌더링을 주의깊게 관찰하자
초기 로드 시에는 괜찮지만 상태 변경 후 이상해진다면:
- CSS 속성의 재적용을 의심하자
- Stacking context 생성/제거를 확인하자
- React DevTools로 재렌더링을 추적하자
4. overflow-hidden과 transform은 상극일 수 있다
overflow-hidden과 transform을 함께 사용할 때는:
- Transform이 정말 필요한지 확인
- Stacking context 생성을 최소화
- 대안으로
clip-path고려
🔧 추가 팁
디버깅 도구
/* CSS로 stacking context 확인하기 */.debug-stacking { /* 새로운 stacking context를 생성하는 속성들 */ outline: 2px solid red;}
.debug-stacking[style*="transform"] { outline-color: blue;}Tailwind 설정
module.exports = { // transform 대신 개별 속성 사용 theme: { extend: { // 필요한 transform만 명시적으로 정의 } }}📝 결론
이 문제를 해결하면서 다음을 깨달았습니다:
- 단순함이 최고: 불필요한 CSS 클래스는 독이 될 수 있다
- 문서를 읽자: Tailwind와 CSS 스펙 문서는 보물창고
- 재현성이 중요: “왜 특정 상황에서만?”이라는 질문이 해답의 열쇠
- 근본 원인 파악: 증상이 아닌 원인을 찾아야 진짜 해결
이제 다운로드 버튼은 어떤 상태에서도 완벽하게 작동합니다! 🎉