
저번 프로젝트에서 렌딩 페이지를 제작할때 정적인 상태의 랜딩 페이지가 차분하긴 했지만 이번 프로젝트는 동적인 느낌을 주고 싶었다. 찾아 보니 Framer Motion라는 라이브러리가 있다는것을 알게 되어 렌딩페이지에 애니메이션을 적용 해보기로 했다.
Framer Motion 란 무엇인가?
Framer Motion은 React를 위한 생산성 높은 애니메이션 라이브러리로, CSS나 JavaScript보다 간단하고 직관적으로 동작의 전환을 구현할 수 있도록 돕는다.
| 개념 | 설명 |
|---|---|
| motion.div | 애니메이션 가능한 div로 변환 |
| initial | 컴포넌트 첫 진입 시 상태 |
| animate | 현재 상태 (애니메이션 적용 대상) |
| exit | 컴포넌트 unmount 시 상태 |
| transition | 전환 시간, 딜레이, 속도 곡선 설정 |
| whileHover / whileTap | 마우스 오버/클릭 시 동작 |
| variants | 여러 상태를 분리 관리할 수 있는 방법 (선택적) |
애니메이션 랜딩 페이지 구현 트러블 슈팅
처음에는 모든 섹션에 Framer Motion으로 진입 애니메이션을 적용했을 때, 페이지 전체가 길어졌고, 스크롤을 내리기 전 아직 보이지 않은 섹션에서도 애니메이션이 이미 끝난 상태로 나타나는 현상이 발생했다.
스크롤을 내리기 전 콘텐츠가 보이지 않음에도 불구하고 animate가 바로 실행됨
결과적으로 사용자가 보게 되는 순간엔 이미 정적인 상태로 완료돼 있어, 기대한 동적인 연출이 사라짐
사용자가 스크롤로 해당 섹션에 도달했을 때만 애니메이션이 시작되었다. 처음에는 단순히 initial / animate만으로 충분할 거라 생각했지만, 실전에서는 사용자의 시점(스크롤 위치)을 고려한 트리거 설계가 얼마나 중요한지 체감할 수 있었다.
이번 프로젝트에서 공통 컴포넌트로 input 과 버튼을 담당하게 되었다. 기본 베이스가 되는 컴포넌트를 제작하고 베이스를 사용하여 다른 인풋과 버튼들의 사용성과 유지보수성을 높이기 위한 폼 UI 컴포넌트 설계 하고자 했다.
기본 Input 컴포넌트 설계
Input 확장 컴포넌트 (SearchInput)
기본 Button 컴포넌트 설계
PickerButton – 선택 토글형 버튼 컴포넌트 설계
FloatingButton – 아이콘 중심의 플로팅 버튼
재사용 가능한 Input / Button 컴포넌트 구현 트러블 슈팅
className을 props로 받아 추가 스타일을 적용하는데, 컴포넌트 내부에서 지정한 Tailwind 클래스와 충돌하면서 원하지 않는 스타일이 적용되는 문제가 발생함.
twMerge 란 무엇인가?
twMerge는 Tailwind CSS의 중복 유틸리티 클래스를 병합할 때, 같은 속성 계열의 클래스 중 마지막 하나만 남기도록 처리해주는 함수. 주로 clsx 또는 classnames과 함께 쓰이며, tailwind-merge 라이브러리에서 제공.
twMerge 왜 필요한가?
Tailwind CSS는 다음과 같은 방식으로 스타일을 지정
문제는 컴포넌트 내부 스타일과 props로 전달받은 className이 충돌할 때 발생
| 항목 | clsx | twMerge |
|---|---|---|
| 목적 | 조건부 className 병합 | Tailwind 클래스 병합 최적화 |
| 중복 처리 | ❌ 없음 ("text-sm text-lg" 둘 다 남음) |
✅ 있음 ("text-lg"만 남음) |
| Tailwind 친화성 | ❌ 없음 | ✅ Tailwind 전용 설계 |
| 사용 추천 방식 | twMerge(clsx(...)) 조합으로 함께 사용 |
깔끔하고 안전한 className 병합, 스타일 충돌 없이 유연한 컴포넌트 완성
버튼의 color, size, rounded, disabled, icon 유무 등 여러 상태를 분기하다 보니, 클래스 조합이 길고, 유지보수하기 어려운 구조가 되어가던 중이었다.
버튼 컴포넌트 내부에서 color, size, disabled, rounded 등 상태별로 스타일을 구분된 객체로 분리함으로써, 조건 분기 복잡도를 낮추고 가독성과 유지보수성을 개선할 수 있었다.
▪ 적용 예시
▪ 최종 병합
버튼 컴포넌트 하나에 너무 많은 역할을 부여하다 보니, 다양한 상태 조합을 감당하기 위해 className 분기 구조가 점점 비대해졌고, 결국 단순한 스타일 변경조차 로직을 복잡하게 수정해야 하는 구조가 되어 있었다. 처음에는 재사용성과 유연성을 추구했지만, 오히려 확장성과 유지보수성을 갉아먹는 구조가 되어버렸음을 느꼈다.
이번 COWORKERS 프로젝트에서는 게시판 기능을 구현하면서, 처음으로 React Query를 도입하게 되었다. 서버에서 데이터를 불러오는 방식에 대해 단순한 fetch와 상태 저장이 아닌, 캐싱과 무한 스크롤을 어떻게 조화롭게 연결할 수 있을까를 고민하게 되었고, 자연스럽게 useInfiniteQuery와 Intersection Observer를 활용한 무한 스크롤 방식을 선택하게 되었다.
처음엔 비교적 간단한 구현처럼 보였지만, 실제로 구현에 들어가 보니 데이터 흐름, 상태 조건, 타이밍, 사용자 경험(UX) 등 다양한 부분에서 세밀한 설계가 필요했다.
React Query란?
서버 상태를 효율적으로 관리할 수 있도록 도와주는 React 전용 라이브러리. 데이터를 불러오고, 캐싱하고, 자동 갱신하며, 상태 변화에 따라 UI를 업데이트하는 과정을 단순화해준다.
| 항목 | 설명 |
|---|---|
| 자동 캐싱 | 같은 요청은 다시 불러오지 않고 캐시된 데이터 재사용으로 불필요한 네트워크 요청 방지 |
| 백그라운드 갱신 | 데이터가 오래되면 자동으로 최신 상태로 갱신되어 사용자는 항상 최신 데이터를 볼 수 있음 |
| 로딩/에러 상태 내장 | `isLoading`, `isError`, `isFetching` 등을 통해 로딩 및 에러 상태를 쉽게 관리할 수 있음 |
| 무한 스크롤 지원 | `useInfiniteQuery`를 통해 페이지네이션 구조의 데이터를 점진적으로 불러올 수 있음 |
무한 스크롤 구조
[화면 진입]
↓
useInfiniteQuery → 첫 데이터 불러옴
↓
ArticleCard 컴포넌트 목록 렌더링
↓
마지막 요소가 뷰포트에 들어오면 (useInView로 감지)
↓
useInfiniteScroll 훅에서 fetchNextPage() 실행
↓
다음 페이지 데이터 가져옴 → 리스트에 추가
COWORKERS 프로젝트 마치며 느낀 점
이번 프로젝트는 나에게 있어 새로운 기술을 직접 경험하고, 적용해본 실험의 장이었다.
특히 Framer Motion을 활용한 애니메이션, React Query 기반의 데이터 처리, Tailwind CSS를 통한 반응형 디자인, 그리고 맛만 본 Firebase.. 이전까지는 문서로만 봐왔던 도구들을 실제로 써보면서 많은 것을 배울 수 있었다.
특히 이번 프로젝트에서 가장 크게 좋다고 느꼈던 React Query!!!! 그동안 직접 상태를 관리하며 fetch 로직을 짜오던 방식과는 완전히 다른 패러다임을 제시했다. 사람들이 왜 좋다좋다 이야기가 나오는지 알게 되었다. 자동 캐싱, 백그라운드 업데이트, 상태별 처리 등으로 코드는 간결해 졌다. 처음 써보는 터라 강의도 여러번 보고 우리 GPT 선생님을 많이 대면하게 했지만 그만큼 값진 경험 이었다.
2번째로 좋았던 Tailwind를 빼놓을 수 없다. Tailwind를 처음 사용할 땐 class명이 너무 길게 나열되는 것 같아 당황했지만, 점점 익숙해질수록 스타일링을 컴포넌트 수준에서 조립하듯 설계할 수 있다는 게 큰 장점으로 다가왔다. 특히 반응형 클래스(tablet:, desktop: 등)나 조건부 스타일 적용이 디자인 시스템 없이도 일정한 UI 흐름을 유지하는 데 매우 효과적이었다. (그리고 번거롭게 css 파일을 왔다 갔다 할 필요가 없다는게 정말 매우! 많이! 좋았다.)