(프론트엔더의 고통)애니메이션부터 SEO까지, 4개 프로젝트로 배운 것들
프론트엔드 삽질기: 애니메이션부터 SEO까지, 4개 프로젝트로 배운 것들
최근 몇 달간 진행한 프론트엔드 프로젝트들을 되돌아보니, 뭔가 패턴이 보이기 시작했다. 리퍼런스 웹사이트 분석하다가 100DaysOfUI 만들고, 거기서 스크롤 애니메이션 삽질하다가 SEO 최적화까지… 겉보기엔 전혀 관련 없어 보이는 작업들이 묘하게 연결되면서 하나의 큰 그림을 그려내고 있었다.
시작은 남의 코드 분석부터
리퍼런스 웹사이트 예제 분석
graph LR
A[Gatsby SSG] --> B[GSAP ScrollTrigger]
B --> C[Contentful CMS]
C --> D[컴포넌트 시스템]
D --> E[애니메이션 패턴]
예제로 본 웹사이트를 보자마자 “어떻게 만든 거지?“라는 생각이 들었다. Gatsby 기반의 정적 사이트인데 GSAP ScrollTrigger로 부드러운 애니메이션을 구현해놨더라.
특히 인상 깊었던 건 컴포넌트 구조였다:
- Header, Menu, Hero, Gallery, Events, Contact 등 명확한 역할 분담
- Contentful CMS 연동으로 콘텐츠와 코드 분리
- GSAP 애니메이션이 React 생명주기와 자연스럽게 통합
“아, 이런 식으로 애니메이션과 CMS를 조합할 수 있구나.” 이때부터 뭔가 계획이 생기기 시작했다.
100DaysOfUI: 컴포넌트 시스템의 실험장
남의 코드 구경만 하고 있을 순 없지. 직접 만들어봐야 진짜 이해가 된다는 생각으로 100DaysOfUI 프로젝트를 시작했다.
아키텍처 설계: 확장성을 고려한 구조
100DaysOfUI/
├── src/
│ ├── components/
│ │ ├── Day49/ # 독립적인 테마와 스타일
│ │ ├── Day53/ # 각자의 컴포넌트 생태계
│ │ └── Day59/ # 스크롤 애니메이션 특화
│ ├── utils/
│ │ ├── use-media-query.tsx # 공통 훅
│ │ └── commonRootUILayouts.scss # 공통 스타일
│ └── styles/
│ └── tailwindImport.css
여기서 핵심 아이디어는 컴포넌트 격리였다. 각 Day 컴포넌트가 완전히 독립적인 테마를 가지면서도, 공통 유틸리티는 재사용할 수 있도록 설계했다.
// 각 컴포넌트마다 독립적인 테마 적용
useEffect(() => {
document.documentElement.setAttribute('data-theme', 'day59-theme');
return () => {
document.documentElement.removeAttribute('data-theme');
};
}, []);
이렇게 하니까 하나의 프로젝트 안에서 완전히 다른 디자인 언어를 가진 컴포넌트들을 공존시킬 수 있었다. 각자의 개성은 살리면서 코드 중복은 최소화하는 거지.
Day59: 스크롤 애니메이션의 지옥
그런데 Day59 컴포넌트를 만들면서 진짜 삽질이 시작됐다. 스크롤 애니메이션이라는 게 생각보다 훨씬 복잡하더라.
라이브러리 호환성 지옥
처음엔 ReactLenis 같은 래퍼 라이브러리를 쓰려고 했는데, Next.js 예제를 Vite 환경으로 옮기는 과정에서 온갖 문제가 터졌다.
// 이런 식으로 직접 인스턴스화하는 게 답이었다
const lenisInstance = new Lenis({
duration: 1.2,
smooth: true,
});
// GSAP ScrollTrigger와 수동으로 연결
lenisInstance.on('scroll', ScrollTrigger.update);
래퍼 라이브러리의 편의성을 포기하고 원본 라이브러리를 직접 제어하니까 오히려 더 안정적이었다. “편의성과 제어권은 트레이드오프다”는 걸 몸소 체험했다.
애니메이션 타이밍의 마술
// 섹션별로 독립적인 스크롤 트리거 설정
const tl1 = gsap.timeline({
scrollTrigger: {
trigger: imagesSectionRef.current,
start: 'top top',
end: '33% bottom',
scrub: true,
}
});
const tl2 = gsap.timeline({
scrollTrigger: {
trigger: heroSectionRef.current,
start: 'top 80%',
end: 'bottom 20%',
}
});
각 섹션마다 다른 타이밍으로 애니메이션이 트리거되도록 세밀하게 조정하는 작업이 정말 까다로웠다. 픽셀 단위로 조정하면서 “이게 맞나?” 싶었는데, 결과물 보니까 그만한 가치가 있더라.
antiperfectclub.github.io: SEO의 현실
애니메이션에 빠져있다가 문득 깨달았다. “아무리 예쁘게 만들어도 검색에 안 걸리면 의미 없잖아?”
React SPA의 SEO 딜레마
// React Helmet Async로 페이지별 메타태그 관리
<SEOHead
title="Portfolio - Frontend Developer"
description="프론트엔드 개발자의 포트폴리오"
textContent={pageTextContent}
noVisualElements={true}
/>
여기서 핵심은 noVisualElements={true} 부분이다. 아무리 화려한 애니메이션을 만들어도 검색 엔진은 텍스트 콘텐츠만 읽는다는 걸 받아들여야 했다.
Google Analytics와 GTM 통합
// Google Tag Manager + Analytics 4 조합
useEffect(() => {
gtag('config', 'GA_MEASUREMENT_ID', {
page_title: document.title,
page_location: window.location.href,
});
}, [pathname]);
데이터 수집도 중요하지만, 무엇보다 사용자 경험을 해치지 않는 선에서 최적화하는 게 관건이었다.
삽질하면서 깨달은 패턴들
1. 아키텍처는 단순함에서 시작한다
graph TD
A[단순한 컴포넌트] --> B[독립적인 테마]
B --> C[공통 유틸리티]
C --> D[확장 가능한 구조]
처음엔 복잡한 설계를 하려고 했는데, 결국 가장 단순한 구조가 가장 유지보수하기 좋았다.
2. 라이브러리 선택의 철학
- 래퍼보다 원본: 편의성보다 제어권이 중요할 때가 많다
- 점진적 향상: HTML/CSS 기본기 → 애니메이션 레이어 추가
- 호환성 우선: 최신 기술보다 안정성이 때로는 더 중요하다
3. 성능과 경험의 균형
// 애니메이션 성능 최적화
const tl = gsap.timeline({
scrollTrigger: {
trigger: ref.current,
start: 'top 80%',
toggleActions: 'play none none reverse',
}
});
화려한 애니메이션도 좋지만, 60fps를 유지하면서 부드럽게 작동하는 게 더 중요하다.
데이터 플로우: 전체 그림
User Interaction
│
▼
Lenis Manager
│
├─── GSAP ScrollTrigger ───► 이미지 스택 애니메이션
│
├─── Framer Motion ────────► Hero 텍스트 애니메이션
│
└─── CSS Variables ────────► 테마 시스템
│
Analytics Event ◄───────────────┘
│
▼
Google Analytics / GTM
각각 따로 만든 것 같지만, 결국 하나의 데이터 플로우로 연결되더라. 사용자의 스크롤 입력이 애니메이션을 트리거하고, 그게 분석 이벤트로 수집되는 전체적인 흐름이 보이기 시작했다.
그래서 뭘 배웠나?
4개 프로젝트를 거치면서 깨달은 건, 프론트엔드 개발은 결국 트레이드오프의 연속이라는 것이다.
- 편의성 vs 제어권
- 성능 vs 시각적 효과
- SEO vs 인터랙티브