COWORKERS 프로젝트 회고

Kingdwan·2024년 10월 31일
post-thumbnail

애니메이션과 반응형 디자인이 적용된 랜딩 페이지 구현

저번 프로젝트에서 렌딩 페이지를 제작할때 정적인 상태의 랜딩 페이지가 차분하긴 했지만 이번 프로젝트는 동적인 느낌을 주고 싶었다. 찾아 보니 Framer Motion라는 라이브러리가 있다는것을 알게 되어 렌딩페이지에 애니메이션을 적용 해보기로 했다.

구현 목표

  • Framer Motion 라이브러리 사용 뷰포트 진입 시 부드러운 진입 애니메이션으로 콘텐츠 표현
  • 테일윈드 사용 (테일윈드의 첫 도전)

Framer Motion 란 무엇인가?

Framer Motion은 React를 위한 생산성 높은 애니메이션 라이브러리로, CSS나 JavaScript보다 간단하고 직관적으로 동작의 전환을 구현할 수 있도록 돕는다.

사용방법

개념 설명
motion.div 애니메이션 가능한 div로 변환
initial 컴포넌트 첫 진입 시 상태
animate 현재 상태 (애니메이션 적용 대상)
exit 컴포넌트 unmount 시 상태
transition 전환 시간, 딜레이, 속도 곡선 설정
whileHover / whileTap 마우스 오버/클릭 시 동작
variants 여러 상태를 분리 관리할 수 있는 방법 (선택적)

애니메이션 랜딩 페이지 구현 트러블 슈팅

문제 상황

처음에는 모든 섹션에 Framer Motion으로 진입 애니메이션을 적용했을 때, 페이지 전체가 길어졌고, 스크롤을 내리기 전 아직 보이지 않은 섹션에서도 애니메이션이 이미 끝난 상태로 나타나는 현상이 발생했다.

  • 스크롤을 내리기 전 콘텐츠가 보이지 않음에도 불구하고 animate가 바로 실행됨

  • 결과적으로 사용자가 보게 되는 순간엔 이미 정적인 상태로 완료돼 있어, 기대한 동적인 연출이 사라짐

원인 분석

  • Framer Motion의 initial → animate는 컴포넌트가 마운트되자마자 바로 실행됨
    따라서 뷰포트에 실제로 보여지는 것과는 무관하게 애니메이션이 진행되는 문제 발생 이는 페이지가 길어질수록, 또는 느리게 스크롤할수록 더 뚜렷하게 드러남

해결 방안

◾ IntersectionObserver 기반 커스텀 훅 작성

◾ 애니메이션 컴포넌트에 적용

결과 및 느낀점

사용자가 스크롤로 해당 섹션에 도달했을 때만 애니메이션이 시작되었다. 처음에는 단순히 initial / animate만으로 충분할 거라 생각했지만, 실전에서는 사용자의 시점(스크롤 위치)을 고려한 트리거 설계가 얼마나 중요한지 체감할 수 있었다.

재사용 가능한 Input / Button 컴포넌트 구현

이번 프로젝트에서 공통 컴포넌트로 input 과 버튼을 담당하게 되었다. 기본 베이스가 되는 컴포넌트를 제작하고 베이스를 사용하여 다른 인풋과 버튼들의 사용성과 유지보수성을 높이기 위한 폼 UI 컴포넌트 설계 하고자 했다.

구현 목표

  • 입력 필드의 일관성과 확장성을 갖춘 Input 컴포넌트 설계
  • 스타일, 상태, 기능이 유연하게 조합 가능한 Button 컴포넌트 구성
  • clsx의 사용방법 숙달

기본 Input 컴포넌트 설계

  • 공통된 스타일은 baseClasses로 묶고 상태별 스타일은 조건부 조합
  • clsx를 활용해 코드 가독성과 재사용성 확보
  • 모든 Input에 UX적으로 동일한 인터랙션 제공

Input 확장 컴포넌트 (SearchInput)

  • Input 기본 컴포넌트에 아이콘을 absolute로 오버레이
  • 패딩/위치 조정을 위한 pl-48, transform 조합
  • 유지보수 시 Input UI를 변경하더라도 SearchInput은 자동 연동됨

기본 Button 컴포넌트 설계

  • 색상, 텍스트 크기, 둥근 모서리 여부까지 props로 분기
  • twMerge를 활용해 클래스 병합의 안정성과 가독성 확보

PickerButton – 선택 토글형 버튼 컴포넌트 설계

  • 내부 상태에 따라 스타일을 동적으로 변경
  • 사용자 인터랙션에 따른 피드백 명확하게 구현

FloatingButton – 아이콘 중심의 플로팅 버튼

  • 아이콘/텍스트 조합 가능한 컴포넌트
  • primary/outlined 컬러 타입 제공 + disabled 대응

재사용 가능한 Input / Button 컴포넌트 구현 트러블 슈팅

문제 상황 1 Tailwind class 중복으로 인한 스타일 깨짐 현상

className을 props로 받아 추가 스타일을 적용하는데, 컴포넌트 내부에서 지정한 Tailwind 클래스와 충돌하면서 원하지 않는 스타일이 적용되는 문제가 발생함.

원인 분석

  • Tailwind CSS는 ‘동일 속성의 클래스’가 겹칠 경우, 따라서 뷰포트에 실제로 보여지는 것과는 무관하게 애니메이션이 진행되는 문제 발생 이는 페이지가 길어질수록, 또는 느리게 스크롤할수록 더 뚜렷하게 드러남
  • props.className을 단순히 병합하면, 예상과 다르게 외부 클래스가 내부 스타일을 덮어버릴 수 있음

해결 방안

◾ twMerge 도입

twMerge 란 무엇인가?

twMerge는 Tailwind CSS의 중복 유틸리티 클래스를 병합할 때, 같은 속성 계열의 클래스 중 마지막 하나만 남기도록 처리해주는 함수. 주로 clsx 또는 classnames과 함께 쓰이며, tailwind-merge 라이브러리에서 제공.

twMerge 왜 필요한가?

Tailwind CSS는 다음과 같은 방식으로 스타일을 지정

문제는 컴포넌트 내부 스타일과 props로 전달받은 className이 충돌할 때 발생

사용 예시

twMerge vs clsx 비교

항목 clsx twMerge
목적 조건부 className 병합 Tailwind 클래스 병합 최적화
중복 처리 ❌ 없음
("text-sm text-lg" 둘 다 남음)
✅ 있음
("text-lg"만 남음)
Tailwind 친화성 ❌ 없음 ✅ Tailwind 전용 설계
사용 추천 방식 twMerge(clsx(...)) 조합으로 함께 사용

✔ twMerge + clsx 를 조합한 프로젝트 개선 코드

  • clsx(...) 조건부 클래스 (size, color, rounded 등) 설정
  • className 외부에서 전달한 스타일 덮어쓰기 허용
  • twMerge(...) 중복된 Tailwind 클래스 자동 병합 (예: text-sm vs text-lg)

결과

깔끔하고 안전한 className 병합, 스타일 충돌 없이 유연한 컴포넌트 완성

문제 상황 2 상태별 스타일 분기가 지나치게 복잡해진 문제

버튼의 color, size, rounded, disabled, icon 유무 등 여러 상태를 분기하다 보니, 클래스 조합이 길고, 유지보수하기 어려운 구조가 되어가던 중이었다.

원인 분석

  • 하나의 컴포넌트로 모든 걸 처리하려다 생긴 복잡도 폭발
    ① 재사용성을 극대화하려는 의도에서 시작
    ② 그 결과, 상태 분기 로직이 무한히 늘어남
    ③ 결국, className 조합이 조건 블록 투성이가 되고
    ④ 단순한 UI 변경 하나도 다양한 조건을 고려해야만 변경 가능
    ⑤ 유지보수 부담 급증

해결 방안

◾ 상태별 스타일 객체 분리 (예: colorStyle, sizeStyle, disabledStyle 등)

버튼 컴포넌트 내부에서 color, size, disabled, rounded 등 상태별로 스타일을 구분된 객체로 분리함으로써, 조건 분기 복잡도를 낮추고 가독성과 유지보수성을 개선할 수 있었다.

▪ 적용 예시

▪ 최종 병합

  • 불필요한 중첩 조건 제거
  • 스타일 로직을 독립된 객체로 분리해 가독성 및 재사용성 확보
  • twMerge를 활용해 중복된 Tailwind 클래스 자동 병합 처리

결과 및 느낀점

버튼 컴포넌트 하나에 너무 많은 역할을 부여하다 보니, 다양한 상태 조합을 감당하기 위해 className 분기 구조가 점점 비대해졌고, 결국 단순한 스타일 변경조차 로직을 복잡하게 수정해야 하는 구조가 되어 있었다. 처음에는 재사용성과 유연성을 추구했지만, 오히려 확장성과 유지보수성을 갉아먹는 구조가 되어버렸음을 느꼈다.

게시글 / 댓글 무한 스크롤

이번 COWORKERS 프로젝트에서는 게시판 기능을 구현하면서, 처음으로 React Query를 도입하게 되었다. 서버에서 데이터를 불러오는 방식에 대해 단순한 fetch와 상태 저장이 아닌, 캐싱과 무한 스크롤을 어떻게 조화롭게 연결할 수 있을까를 고민하게 되었고, 자연스럽게 useInfiniteQuery와 Intersection Observer를 활용한 무한 스크롤 방식을 선택하게 되었다.
처음엔 비교적 간단한 구현처럼 보였지만, 실제로 구현에 들어가 보니 데이터 흐름, 상태 조건, 타이밍, 사용자 경험(UX) 등 다양한 부분에서 세밀한 설계가 필요했다.

구현 목표

  • 게시글 및 댓글 목록에 대해 스크롤 기반의 점진적 데이터 로딩 구현
  • 사용자는 페이지를 새로고침하거나 버튼을 누르지 않아도 자연스럽게 다음 페이지 로드
  • 불필요한 API 요청 최소화 및 백엔드 성능 부담 완화

React Query – 첫 도입 및 데이터 캐싱

React Query란?

서버 상태를 효율적으로 관리할 수 있도록 도와주는 React 전용 라이브러리. 데이터를 불러오고, 캐싱하고, 자동 갱신하며, 상태 변화에 따라 UI를 업데이트하는 과정을 단순화해준다.

💡 React Query의 주요 기능 & 장점

항목 설명
자동 캐싱 같은 요청은 다시 불러오지 않고 캐시된 데이터 재사용으로 불필요한 네트워크 요청 방지
백그라운드 갱신 데이터가 오래되면 자동으로 최신 상태로 갱신되어 사용자는 항상 최신 데이터를 볼 수 있음
로딩/에러 상태 내장 `isLoading`, `isError`, `isFetching` 등을 통해 로딩 및 에러 상태를 쉽게 관리할 수 있음
무한 스크롤 지원 `useInfiniteQuery`를 통해 페이지네이션 구조의 데이터를 점진적으로 불러올 수 있음

React Query 사용 예시

① API 호출 함수 만들기

  • Axios를 사용하여 실제 HTTP 요청 처리
  • get, post, patch, delete 등 REST 방식에 맞게 분리
  • 순수한 요청 함수 → 재사용성 높음

② 쿼리 훅 만들기

  • useQuery, useInfiniteQuery 등 조회(GET) 중심
  • queryKey로 캐시 구분 및 제어
  • getNextPageParam을 통한 무한스크롤 구현

③ 뮤테이션 훅 만들기

  • useMutation으로 생성, 수정, 삭제 등 쓰기 작업(POST, PATCH, DELETE) 처리
  • onSuccess에서 invalidateQueries를 활용해 캐시 갱신

무한 스크롤 구조

[화면 진입]

useInfiniteQuery → 첫 데이터 불러옴

ArticleCard 컴포넌트 목록 렌더링

마지막 요소가 뷰포트에 들어오면 (useInView로 감지)

useInfiniteScroll 훅에서 fetchNextPage() 실행

다음 페이지 데이터 가져옴 → 리스트에 추가

무한 스크롤 프로젝트 내 구현 동작 순서

1. 정렬 상태 정의 및 초기값 설정

  • orderBy: 사용자가 선택한 정렬 기준 (recent 또는 like)
  • pageSize: 한 페이지당 몇 개의 게시글을 보여줄지 설정

2. 데이터 패칭 (React Query - useInfiniteQuery)

  • 🔑 queryKey: 캐싱 키 (정렬 방식과 검색어에 따라 분리)
  • 🔍 queryFn: 서버로부터 데이터 패칭
  • 🔁 getNextPageParam: 다음 페이지 여부 판단
  • 🔄 fetchNextPage(): 다음 데이터 요청

#### 3. 마지막 요소 감지 (Intersection Observer)

  • inView: 마지막 요소가 뷰포트에 들어왔는지 여부
  • lastArticleRef: 마지막 게시글에 부착되는 감지용 ref

4. 무한 스크롤 로직 분리 (커스텀 훅)

  • 커스텀 훅 내부에서는 다음과 같은 조건으로 fetchNextPage() 실행


5. 데이터 평탄화 + 정렬

  • 페이지 별로 받아온 데이터 pages를 1차 배열로 펼침
  • 좋아요 순 정렬 적용 (조건부)

6. 렌더링 및 ref 연결

  • ref={lastArticleRef}는 마지막 게시글에만 연결
  • 이게 화면에 보이면 inView === true → 다음 페이지 로딩

6. 부가 상태 처리

  • 로딩 상태, 검색 결과 없음, 다음 페이지 로딩 시 UI 처리

COWORKERS 프로젝트 마치며 느낀 점

이번 프로젝트는 나에게 있어 새로운 기술을 직접 경험하고, 적용해본 실험의 장이었다.
특히 Framer Motion을 활용한 애니메이션, React Query 기반의 데이터 처리, Tailwind CSS를 통한 반응형 디자인, 그리고 맛만 본 Firebase.. 이전까지는 문서로만 봐왔던 도구들을 실제로 써보면서 많은 것을 배울 수 있었다.

특히 이번 프로젝트에서 가장 크게 좋다고 느꼈던 React Query!!!! 그동안 직접 상태를 관리하며 fetch 로직을 짜오던 방식과는 완전히 다른 패러다임을 제시했다. 사람들이 왜 좋다좋다 이야기가 나오는지 알게 되었다. 자동 캐싱, 백그라운드 업데이트, 상태별 처리 등으로 코드는 간결해 졌다. 처음 써보는 터라 강의도 여러번 보고 우리 GPT 선생님을 많이 대면하게 했지만 그만큼 값진 경험 이었다.

2번째로 좋았던 Tailwind를 빼놓을 수 없다. Tailwind를 처음 사용할 땐 class명이 너무 길게 나열되는 것 같아 당황했지만, 점점 익숙해질수록 스타일링을 컴포넌트 수준에서 조립하듯 설계할 수 있다는 게 큰 장점으로 다가왔다. 특히 반응형 클래스(tablet:, desktop: 등)나 조건부 스타일 적용이 디자인 시스템 없이도 일정한 UI 흐름을 유지하는 데 매우 효과적이었다. (그리고 번거롭게 css 파일을 왔다 갔다 할 필요가 없다는게 정말 매우! 많이! 좋았다.)

0개의 댓글