웹디자인 디버깅보다 더 머리를 쥐뜯게 만드는 웹 애니메이션 디버깅
– ArtXTech
모니터에 대고 울부짖는 당신에게: 왜 내 애니메이션은 보이지도 않고 지랄인가?
아, 또 시작이다. 야심 차게 코드를 수정했는데, 화면에서는 아무 일도 일어나지 않는 그 상황.
특히 애니메이션. 분명히 뭔가 움직여야 하는데 감감무소식일 때, 개발자 도구를 열어보면 요소 혼자 저세상 어딘가에서 미친 듯이 픽셀 값을 바꾸고 있는 광경을 목격하게 된다.
“Explore More” 스크롤 마크가 보이지 않는다고? 요소는 무한히 위아래로 움직이는데 시각적으로는 감감무소식이라고? 축하한다. 당신은 전형적인 프론트엔드 개발 지옥의 한가운데에 떨어진 것이다.
이 지옥 같은 디버깅 과정을 어떻게 헤쳐나가야 할지…
이하의 내용들은 약간의 항마력이 필요합니다. 왜 굳이 저딴 말투로 썼냐면, 걍 제가 문제 이슈마다 아이템 정리하고 혼자 해답 찾는 노트들을 보니까, 약간 미쳐있었는지 무슨 이세계로 떨어진 마왕의 세계 찬탈기마냥 흑염룡이 날뛰어있더라고요. 님들은 걍 읽을 때만 항마력을 가지면 되지만 쓰는 지가 싸놓은 똥을 다시 줏어 글로 쓰는 저는 어땠겠어요…ㅠ
문제의 발단: 보이지 않는 애니메이션의 정체
Q: 코드를 고쳤는데 ‘Explore More’ 스크롤 마크가 페이지 로드 시 보이지 않습니다. 개발자 도구로 확인해보니, 해당 요소는 실제로 무한히 위아래로 움직이고 있는데, 시각적으로만 나타나지 않아요. 제 코드가 원본과 어떻게 다르길래 이런 문제가 발생하는 걸까요?
A: 원본 코드와 코드를 비교해보면 문제의 원인이 명확하게 드러난다.
-
애니메이션 초기 상태:
- 원본: 멀쩡히 보이는 상태 (
opacity: 1; visibility: inherit;) 에서 시작한다. - 코드: 시작부터 투명하고(
opacity: 0), 보이지도 않으며(visibility: hidden), 심지어transform으로 저 멀리 화면 밖(translate(0px, -1000px))으로 날려 보낸 상태에서 시작한다. 시작부터 존재를 지워버렸는데 보이길 바라는 건가?
- 원본: 멀쩡히 보이는 상태 (
-
CSS 애니메이션 이름:
- 원본:
move라는 예측 가능한 이름을 사용한다. - 코드: CSS 모듈 때문에
_move_nqx9u_1같은, 기계나 좋아할 법한 이름으로 바뀌었다. 뭐, 이건 CSS 모듈의 특성이니 그럴 수 있다 치자.
- 원본:
-
Transform 속성 충돌:
- 원본: 단순하게
transform: rotate(90deg);만 적용되어 있다. - 코드: GSAP 애니메이션이 끝나지 않아서인지, 초기 설정값인
transform: translate(0px, -1000px) rotate(90deg) scale(3, 3);같은 복잡한 변환이 그대로 남아있다. CSS 애니메이션(_move_nqx9u_1)이 열심히top값을 바꾸며 요소를 움직이려 해도, 이 강력한transform값이 모든 걸 덮어쓰고 있으니 요소는 계속 저 멀리 어딘가에 처박혀 있게 되는 거다.
- 원본: 단순하게
결론: 문제는 GSAP 애니메이션이 제대로 완료되지 않아서, 요소가 초기 숨김 상태(opacity: 0, visibility: hidden, transform: translate(...))에서 벗어나지 못하는 것이다. CSS 애니메이션 자체는 돌고 있을지 몰라도, GSAP가 걸어놓은 transform 때문에 시각적으로는 아무 변화가 없는 것처럼 보이는 거다.
디버깅 지옥 탈출 가이드: 뭘 써야 할까?
Q: 그래서 이 문제를 해결하려면 뭘 해야 합니까? 제시된 테스트 방법들이 많던데, 이걸 순서대로 다 적용해야 하나요?
A: 다 하라고? 시간이 남아도는 모양이지? 상황과 선호도에 따라 필요한 도구를 골라 쓰면 된다. 물론, 명백한 원인이 있을 때는 특정 도구들이 더 효과적일 수 있다.
-
가장 먼저: 셀렉터 검증 (Element Selector Validation)
- 이건 기본 중의 기본이다. GSAP가 엉뚱한 요소를 붙잡고 있거나, 클래스 이름을 잘못 입력했을 가능성을 확인하는 거다. 특히 CSS 모듈을 사용하면 클래스 이름이 복잡해지니 오타가 나기 쉽지. 아래 같은 간단한 유틸리티 함수 하나면 된다.
// GSAP 셀렉터가 실제로 DOM 요소를 찾는지 확인하는 함수 export const validateSelectors = (selectors) => { const results = {}; selectors.forEach(selector => { const elements = document.querySelectorAll(selector); results[selector] = { found: elements.length > 0, count: elements.length, // elements: Array.from(elements) // 필요하면 요소 목록도 확인 }; if (elements.length === 0) { console.warn(`⚠️ 경고: 셀렉터 "${selector}"에 해당하는 요소를 찾을 수 없습니다!`); } }); console // GSAP 셀렉터가 실제로 DOM 요소를 찾는지 확인하는 함수 export const validateSelectors = (selectors) => { const results = {}; selectors.forEach(selector => { const elements = document.querySelectorAll(selector); results[selector] = { found: elements.length > 0, count: elements.length, // elements: Array.from(elements) // 필요하면 요소 목록도 확인 }; if (elements.length === 0) { console.warn(`⚠️ 경고: 셀렉터 "${selector}"에 해당하는 요소를 찾을 수 없습니다!`); } }); console.table(results); // 결과를 테이블 형태로 깔끔하게 보여준다. return results; };
이걸 컴포넌트의 useEffect 훅 안에서, 애니메이션 코드를 실행하기 전에 돌려봐라. 콘솔에 어떤 셀렉터가 문제인지 바로 찍힐 거다.
jsx:components/Hero.jsx
import { useEffect } from 'react';
import { gsap } from 'gsap';
import styles from './Hero.module.scss';
import { validateSelectors } from '../utils/animationDebug';
const Hero = () => {
useEffect(() => {
// 애니메이션 시작 전에 셀렉터 검증부터!
const selectorsToValidate = [
`.${styles.leftSlab}`,
`.${styles.rightSlab}`,
`.${styles.animContainer}`,
`.${styles.scrollMark}` // 문제의 그 요소
];
validateSelectors(selectorsToValidate); // 검증 실행
// 이제 GSAP 애니메이션 코드...
let tl = gsap.timeline();
// ... (이하 생략)
}, []);
// ... (컴포넌트 나머지 코드)
};
export default Hero;
-
그 다음 고려할 것: CSS 클래스 이름 검증 (CSS Class Name Validation)
- 셀렉터 검증으로도 못 잡는, 혹은 더 체계적으로 방지하고 싶다면 이 방법을 써라. JSX 코드에서 사용한 클래스 이름이 실제로 CSS 모듈 파일(
.module.scss)에 존재하는지 확인하는 거다. 문제의 직접적인 원인인 ‘클래스 이름 불일치’를 막는 데 효과적인 프랙티스다. 개발 중에만 실행되도록 설정하면 된다.
// JSX에서 사용된 클래스 이름이 CSS 모듈에 실제로 존재하는지 확인하는 함수 export const validateCssModules = (styles, usedClassNames) => { const missingClasses = []; usedClassNames.forEach(className => { // styles 객체에 해당 클래스 이름이 정의되어 있지 않다면? if (!styles[className]) { missingClasses.push(className); console.error(`❌ 에러: 클래스 "${className}"가 JSX에서는 사용되었지만, CSS 모듈 파일에는 정의되지 않았습니다!`); } }); return { valid: missingClasses.length === 0, // 누락된 클래스가 없으면 유효 missingClasses }; };- 이것도 컴포넌트 코드 상단, 개발 모드일 때만 실행되도록 넣으면 된다.
import styles from './Hero.module.scss'; import { validateCssModules } from '../utils/cssValidator'; // 유틸리티 함수 임포트 const Hero = () => { // 개발 모드일 때만 CSS 클래스 이름 검증 실행 if (process.env.NODE_ENV === 'development') { const classNamesInJsx = [ // 이 컴포넌트에서 사용하는 모든 클래스 이름 목록 'hero', 'animContainer', 'left', 'leftSlab', 'right', 'rightSlab', 'bg', 'leftBg', 'rightBg', 'heroContent', 'heroTitle', 'scrollMark' ]; validateCssModules(styles, classNamesInJsx); // 검증 실행 } // ... (컴포넌트 나머지 코드) }; - 셀렉터 검증으로도 못 잡는, 혹은 더 체계적으로 방지하고 싶다면 이 방법을 써라. JSX 코드에서 사용한 클래스 이름이 실제로 CSS 모듈 파일(
대부분 문제는 대부분 이 두 단계에서 걸러진다. 셀렉터 오류나 클래스 이름 오타 같은 기본적인 실수는 생각보다 자주 발생한다. 특히 CSS 모듈이나 복잡한 DOM 구조를 다룰 때는 더더욱.
나머지 테스트 옵션들?
- 애니메이션 상태 로깅 (Animation State Logging): 애니메이션이 중간에 멈추거나 이상하게 동작할 때, 각 단계별 상태를 추적하고 싶으면 유용하다.
- 타임라인 시각화 (Animation Timeline Visualisation): 여러 애니메이션이 얽힌 복잡한 타임라인의 순서나 타이밍 문제를 디버깅할 때 도움이 될 수 있다. 하지만 설정이 좀 귀찮다.
- Visual Regression Testing (Cypress): 이건 레이아웃 깨짐이나 디자인 일관성을 잡는 데는 최고다. 하지만 설정도 복잡하고 유지보수도 귀찮다. 문제처럼 단순히 ‘안 보이는’ 애니메이션을 잡는 데는 과할 수 있다.
- Component-Level Unit Testing (React Testing Library): 이건 컴포넌트의 로직이나 상호작용을 테스트하는 데는 좋지만, 실제 ‘보이는’ 애니메이션이나 레이아웃을 검증하는 데는 한계가 명확하다.
그러니 일단 셀렉터 검증과 CSS 클래스 이름 검증부터 적용해 봐라. 아마 거기서 문제가 해결될 가능성이 높다. 그래도 안 되면… 그때 가서 다른 방법들을 고민해도 늦지 않다. 디버깅은 원래 그런 거니까. 삽질의 연속이지.
레이아웃 박살 감지: 유닛 테스트 vs. 비주얼 테스트
Q: 그럼 컴포넌트 레벨 유닛 테스트는 레이아웃이나 그리드가 깨지는 걸 확인하는 데는 효과가 없다는 건가요? Visual Regression Testing은 설정이 복잡하다고 하고… 레이아웃 검증은 뭘로 해야 하죠?
A: 정확히 봤다. **컴포넌트 레벨 유닛 테스트(React Testing Library 같은 거)**는 레이아웃이나 그리드가 실제로 어떻게 보이는지 확인하는 데는 거의 쓸모가 없다. 왜냐고?
- 실제 렌더링 엔진 부재: Jest 환경(JSDOM)은 브라우저처럼 화면을 그리지 않는다. CSS Grid나 Flexbox가 어떻게 적용돼서 요소들이 배치되는지 알 턱이 있나.
-
CSS 처리 능력 부족: CSS 파일을 읽어서 스타일을 적용하는 능력이 없다.
display: grid?grid-template-columns? 그게 뭔지 모른다. - 크기/위치 확인 불가: 요소가 실제로 얼마나 크고, 화면 어디에 있는지 확인할 방법이 없다.
그럼 유닛 테스트로 뭘 할 수 있냐고?
- 필요한 HTML 요소(div, span 등)가 DOM에 존재하는지.
- 요소에 올바른 클래스 이름이 붙어 있는지.
- 요소들이 올바른 부모-자식 관계로 중첩되어 있는지.
- (간신히) 뷰포트 크기를 시뮬레이션해서 반응형 클래스가 바뀌는지 정도는 확인할 수 있다.
하지만 이걸로는 grid-template-areas가 제대로 작동하는지, 아이템 간격이 맞는지, 요소들이 겹치지는 않는지 같은 실제 레이아웃 문제는 절대 못 잡는다.
반면에, **Visual Regression Testing (Cypress 같은 도구)**은 이런 레이아웃 문제를 잡는 데 특화되어 있다.
- 실제 브라우저 사용: 진짜 브라우저에서 테스트를 돌리니 CSS Grid, Flexbox가 완벽하게 적용된다.
- 픽셀 단위 비교: 이전 스냅샷 이미지와 현재 화면을 픽셀 단위로 비교해서 아주 작은 레이아웃 변화도 잡아낸다.
- 반응형 테스트 용이: 다양한 화면 크기에서 테스트를 돌려 반응형 레이아웃이 깨지지 않는지 쉽게 확인할 수 있다.
- 애니메이션 중 확인 가능: 애니메이션 중간중간 스냅샷을 찍어 레이아웃이 망가지지 않는지 볼 수 있다.
결론: 그리드나 복잡한 레이아웃이 깨지는 걸 확실하게 잡고 싶다면, Visual Regression Testing이 훨씬 효과적인 방법이다. 설정과 유지보수가 귀찮다는 단점은 있지만, 그만한 가치는 있다.
만약 Cypress가 너무 부담스럽다면?
- 타협안: 기본적인 구조는 유닛 테스트로 잡고, 정말 중요한 핵심 레이아웃(예: 메인 그리드, 상품 목록) 몇 개만 골라서 Visual Regression 테스트를 적용하는 거다.
-
더 가벼운 대안:
jest-image-snapshot같은 라이브러리를 Puppeteer와 함께 사용하면 Cypress보다는 가볍게 이미지 스냅샷 테스트를 할 수 있다. 물론, 이것도 설정은 필요하다. - 최후의 수단: 그냥 눈으로 직접 브라우저 개발자 도구의 Grid/Flexbox 검사기를 켜놓고 열심히 확인하는 거다. 원시적이지만, 작은 프로젝트에서는 통할 수도 있다.
그러니 프로젝트의 규모, 팀 상황, 그리고 인내심 수준을 고려해서 적절한 방법을 선택해라.
그래서, 유닛 테스트는 언제 쓰는 건데?
Q: 레이아웃 검증에 별로라면, 컴포넌트 레벨 유닛 테스트는 도대체 언제 써야 유용한 겁니까?
A: 레이아웃 잡는 데는 별로라고 했지, 쓸모없다고는 안 했다. 컴포넌트 레벨 유닛 테스트는 다음과 같은 상황에서 아주 유용하게 쓰인다. 레이아웃보다는 컴포넌트의 내부 로직과 동작을 검증하는 데 초점을 맞춘다고 생각해라.
-
컴포넌트 로직 검증:
- 조건부 렌더링: “로그인 상태면 프로필 버튼 보이기, 아니면 로그인 버튼 보이기” 같은 로직이 제대로 작동하는지.
- Props에 따른 렌더링 변화: 전달된 props 값에 따라 컴포넌트가 다른 내용을 보여주는지.
- 내부 상태 변화: 버튼 클릭 시 카운터 상태가 증가하는지, 토글 스위치가 상태를 제대로 바꾸는지.
-
상호작용 테스트:
- 이벤트 핸들러 호출: 버튼 클릭 시
onClick핸들러가 올바른 인자와 함께 호출되는지. - 폼 제출 및 유효성 검사: 폼 입력 후 제출 시 데이터가 올바르게 처리되는지, 유효성 검사 로직이 작동하는지.
- 키보드 네비게이션: Tab 키를 눌렀을 때 포커스가 예상대로 이동하는지.
- 애니메이션 트리거 확인: 특정 동작 시 애니메이션 시작 함수(예:
gsap.to)가 호출되는지 (애니메이션이 ‘어떻게 보이는지’는 테스트 못 함).
- 이벤트 핸들러 호출: 버튼 클릭 시
-
컴포넌트 API 검증:
- Props 처리: 컴포넌트가 전달받은 props를 제대로 사용하고 있는지.
- 콜백 Props 호출: 부모로부터 받은 콜백 함수가 적절한 시점에 호출되는지.
- (드물지만) 노출된 메서드 확인:
forwardRef등으로 노출된 메서드가 제대로 작동하는지.
-
외부 연동 테스트 (Mocking 활용):
- 상태 관리 라이브러리(Redux, Context API 등) 연결: 컴포넌트가 스토어에 제대로 연결되고 상태를 읽거나 액션을 디스패치하는지.
- API 호출: 데이터 로딩 시 API 호출 함수가 올바른 파라미터로 호출되는지.
- 로딩/에러 상태 처리: 데이터 로딩 중일 때 로딩 스피너가 보이는지, 에러 발생 시 에러 메시지가 표시되는지.
-
접근성 테스트:
- ARIA 속성 확인: 요소에 올바른 ARIA 역할(role), 상태(state), 속성(property)이 부여되었는지.
- 포커스 관리: 모달 창이 열렸을 때 포커스가 모달 내부로 이동하고, 닫혔을 때 이전 위치로 돌아가는지.
- 시맨틱 HTML 구조: 제목(h1-h6), 목록(ul, ol), 버튼(button) 등 의미에 맞는 HTML 태그를 사용했는지. (jest-axe 같은 도구와 함께 사용하면 더 효과적)
요약하자면, 컴포넌트 레벨 유닛 테스트는 컴포넌트가 **‘어떻게 보이는가’**보다는 **‘어떻게 작동하는가’**를 검증하는 데 강점이 있다. 코드 변경 시 기능적인 회귀(regression)를 빠르게 잡아내는 데 매우 유용하지. 그러니 Visual Regression 테스트와 상호 보완적으로 사용하는 것이 가장 이상적이다.
다른 리모션 포스팅을 보고 싶다면
2025-01-16-implementing-canvas-animation-to-remotion-kr
2025-03-18-making-my-own-video-editor-in-progress
KR-no-more-adobe-starting-remotion-kr
KR-remotion-gsap
KR-making-chart-data-journalism-storytelling-motion-setup-with-remotion
KR-figma-to-jitter-and-remotion
KR-converting-bodymovin-animation-to-remotion-template
KR-how-i-make-a-youtube-series-with-remotion-dynamic-srt-script-setup-exp-likejennie
KR-how-i-made-podcast-interview-with-ai-virtual-idol
KR-remotion-and-shader