(좌) 개선한 이미지 뷰어, (우) 기존 불편한 이미지 뷰어

github: 링크

1. 소개 및 배경

  • 프로젝트 배경 및 목적
    • 취업 활동을 위한 사이트 탐색 중 중고차 사이트에서 핵심 기능인 이미지 뷰어가 사용자에게 불편한 경험을 제공하고 있는 것을 발견했습니다. 기존 이미지 뷰어는 다음과 같은 문제점이 있었습니다:취업 활동을 위한 사이트 탐색 중 중고차 사이트임에도 사이트의 핵심 기능인 이미지 뷰어가 사용자에게 불편한 경험을 제공하고 있는 것을 발견했습니다.
  • 기존 이미지 뷰어의 문제점
    • 사용성 문제: 썸네일을 탐색하는 과정에서 하단에 썸네일들이 배치되어 있어 아래로 내리고 다시 위로 올라가서 보고 다시 내리는 불편한 과정이 동반되어 있습니다.
    • 기능적 제한: 해당 뷰어는 기본적인 슬라이드 기능만 제공하고, 확대/축소와 패닝이 매끄럽게 통합되지 않아(휠 zoom 부재) 사용자가 차량의 세부 사항을 확인하기 위해 추가적인 단계를 거쳐야 했습니다.
    • 접근성 부족: 키보드 내비게이션이나 스크린 리더 지원과 같은 접근성 기능이 부족하여, 다양한 사용자 층의 니즈를 충족시키지 못했습니다.
  • 개선 목표
    • 사용자 중심 인터페이스로 직관적인 이미지 탐색 경험 제공
    • 모든 디바이스에서 일관된 사용자 경험 구현
    • 접근성 표준을 준수하여 다양한 사용자층 지원

2. 기술 스택 및 선택 이유

핵심 기술

  • React 19 - 최신 버전의 리액트를 사용하여 컴포넌트 기반 아키텍처 구현 및 최신 기능 활용
  • TypeScript - 타입 안정성 확보로 개발 단계에서 오류 예방 및 코드 품질 향상
  • Vite - 빠른 개발 환경과 효율적인 빌드 프로세스를 위해 선택

UI 및 상호작용

  • Tailwind CSS 4 - 빠른 UI 개발과 일관된 디자인 시스템 적용을 위한 유틸리티 우선 CSS 프레임워크
  • react-zoom-pan-pinch - 이미지 확대/축소 및 패닝 기능을 효율적으로 구현하기 위해 선택, 문서가 상세하게 되어 있음
  • Swiper - 직관적이고 반응형 이미지 슬라이더 구현에 활용, 문서가 상세하게 되어 있음
  • react-icons - 다양한 아이콘 세트를 일관된 인터페이스로 사용 가능

유틸리티 및 최적화

  • react-device-detect - 모바일 사용자에게만 화면 방향 전환 기능을 제공하기 위해 사용
  • Lodash - 디바운싱, 쓰로틀링 등 성능 최적화와 데이터 조작을 위한 유틸리티 함수 활용
  • Zustand - 전역 상태를 관리하여 프롭스 드릴링 제거와 성능 최적화를 위해 사용했습니다.

3. 주요 기능 및 개선점

직관적인 확대/축소

  • PC: 마우스 휠과 더블클릭을 통한 직관적인 확대/축소
  • 모바일: 핀치 제스처를 통한 자연스러운 확대/축소
  • 확대 상태에서 끌어서 이미지 패닝 가능

개선된 썸네일 내비게이션

  • 사용자 접근성을 고려한 썸네일 패널 디자인
  • 현재 이미지와 썸네일 간의 시각적 동기화 제공
  • 확장 가능한 썸네일 패널로 효율적인 이미지 탐색

플랫폼 최적화

  • 화면 방향 제어 지원으로 모바일 사용자 경험 향상

접근성 기능

  • 키보드 내비게이션 완벽 지원 (일반 사용자를 위한 단축키, 스크린리더 사용자를 위한 단축키)
  • 적절한 ARIA 속성과 스크린 리더 호환성
  • 접근성 표준을 준수한 포커스 관리

성능 최적화

  • 스켈레톤 UI: 초기 로딩 시 사용자 대기 경험 개선
  • 이미지 Lazy 로딩: 네트워크 부담 감소
  • 이미지 프리로딩: 다음/이전 이미지를 미리 로드하여 스와이프 시 즉각적인 반응성 제공
  • 렌더링 최적화: 불필요한 리렌더링 방지를 위한 메모이제이션 전략
  • 이벤트 핸들링 최적화: 디바운싱/쓰로틀링을 통한 성능 병목 해소
  • 썸네일 배치 로딩(Batch Loading): 썸네일을 5개씩 나누어 순차적으로 로드하여 네트워크 부하 분산

4. 기술적 구현

├───📁 api/
│   └───📄 imageApi.ts
├───📁 assets/
│   ├───📁 customIcon/
│   │   └───📄 doubleClickIcon.svg
│   └───📄 rotatePhone.svg
├───📁 components/
│   ├───📁 ImageViewer/
│   │   ├───📄 ImageViewer.tsx
│   │   ├───📄 NavigationControls.tsx
│   │   ├───📄 ThumbnailItem.tsx
│   │   └───📄 ThumbnailPanel.tsx
│   ├───📁 SwiperGallery/
│   │   └───📄 SwiperGallery.tsx
│   ├───📁 UI/
│   │   └───📄 Skeleton.tsx
│   ├───📁 reactZoomPanPinch/
│   │   ├───📄 ImageRenderer .tsx
│   │   ├───📄 TransformViwer.tsx
│   │   └───📄 ZoomControls.tsx
│   └───📁 ui/
│       └───📄 tooltip.tsx
├───📁 hooks/
│   ├───📁 ImageViewer/
│   │   ├───📄 useFocusManagement.tsx
│   │   ├───📄 useFullscreen.tsx
│   │   ├───📄 useImageSlider.tsx
│   │   ├───📄 useKeyboardNavigation.tsx
│   │   ├───📄 useScreenOrientation.tsx
│   │   └───📄 useThumbnailLoader.tsx
│   ├───📁 TransformViwer/
│   │   ├───📄 usePanningControl.tsx
│   │   ├───📄 useSwipeMessage.tsx
│   │   ├───📄 useTransformViewerShortcuts.tsx
│   │   └───📄 useZoomControl.tsx
│   ├───📄 useGalleryData.tsx
│   └───📄 useImagePreloader.tsx
├───📁 lib/
│   └───📄 utils.ts
├───📁 store/
│   └───📄 useZoomScreenReaderStore.ts
├───📄 App.tsx
├───📄 index.css
├───📄 main.tsx
└───📄 vite-env.d.ts

전체적인 형태

아키텍처 개요

  • react-zoom-pan-pinch 라이브러리를 사용한 TransformViwer를 swiper를 사용한 SwiperGallery로 감싸는 형태
  • React와 TypeScript를 기반으로 한 모듈화된 컴포넌트 구조
  • 관심사 분리 원칙에 따른 커스텀 훅 설계
  • 상태 관리와 이벤트 처리의 효율적인 분리

핵심 컴포넌트 설명

  • ImageViewer: 전체 뷰어의 컨테이너 역할, 하위 컴포넌트 조율
  • SwiperGallery: 이미지 간 슬라이드 기능 담당
  • TransformViewer: 확대/축소와 패닝 기능 구현
  • ThumbnailPanel: 썸네일 목록 관리 및 표시
  • NavigationControls: 사용자 인터페이스 컨트롤 제공

중요 커스텀 훅 소개

  • useImageSlider: 이미지 슬라이드 기능과 상태 관리
  • useZoomControl: 확대/축소 기능 제어
  • usePanningControl: 패닝 동작 처리 및 경계 감지
  • useKeyboardNavigation: 키보드 내비게이션 지원
  • useScreenOrientation: 모바일 전체 화면 방향 제어
  • useThumbnailLoader: 썸네일 이미지 효율적 로딩

5. 주요 도전과 해결 방법

이미지 확대 시 스와이프 처리

react-zoom-pan-pinch 와 swiper 통합

문제:

이미지가 확대된 상태에서 사용자가 스와이프할 때, 이 동작이 이미지 패닝으로 처리되어야 할지 다음 이미지로 넘어가는 스와이프로 처리되어야 할지 결정하는 과제가 있었습니다.

해결:

  • isZoomed 상태를 추가하여 true일 경우 swiper의 동작을 차단
  • 아이콘을 사용한 직관적인 제스처 가이드 메시지로 사용자 혼란 최소화
    • 아무 정보 없이 행동을 제약당하면 소비자는 당황할 것이기 때문에 줌을 한 상태에서 swipe를 할 때 안내를 해주는 로직 추가
    • 이미지 경계 감지 로직(usePanningControl hook)을 구현, 디바운스를 이용해 성능 최적화

모바일 전체 화면 방향 전환

모바일 화면에서의 전체 화면

문제:

모바일 기기에서 전체 화면 모드를 할 때 큰 화면을 보기 위해서는 자동 회전이 켜져있지 않으면 핸드폰 위의 스크롤을 열고 자동 회전을 킨 다음 다시 돌아와서 핸드폰을 회전시켜야 한다는 불편함이 있음

해결:

  • Screen.orientation API를 활용하여 기능을 구현

지원되는 기기와 모바일 환경에서만 동작 할 수 있도록 조건문을 넣어 웹이나 지원하지 않은 환경에서는 볼 수 없도록 함

성능 최적화

라이트하우스 만점

문제:

초기 렌더링 시 필요없는 리랜더링이 160회 가량 발생함, 이후 다른 작업을 할 때에도 버벅임이 발생

해결:

  • 스켈레톤 UI로 FCP 향상, 초기 이탈 방지
  • 이미지 프리로딩 전략으로 스와이프시 사용자 경험 향상
  • 이벤트 핸들러에 디바운싱과 쓰로틀링 적용
  • React.memo, useMemo, useCallback을 활용한 불필요한 렌더링 방지
  • 이미지 lazy 로딩으로 네트워크 부담 감소
    • 중고차 사이트에서는 고화질 이미지를 사용할 것으로 예상되어 lazy 옵션을 사용하여 스와이프나 썸네일 클릭 시에만 큰 이미지를 불러오게 했습니다.
  • isCurrentImage 변수를 가드를 설치하여 만들어 해당 이미지가 아닌 이벤트들을 막음
  • Zustand의 Selector 패턴을 사용하여 리랜더링 최적화

웹 접근성 개선

(좌) tab으로 전체 탐색 / (우) 포커스 트래핑 기능 구현
(좌) 스크린 리더 모드 On,Off / (우) 키보드로 줌 패닝 제어

문제:

웹접근성을 고려한 코딩이 되지 않아 다양한 사용자층의 이용 불가능

해결:

  • tab index와 short cut으로 모든 기능을 키보드로 사용 가능하게 함, 단축키를 tooltip을 전달
  • 의미있는 순서로 요소들을 배치하여 사용자가 예측 가능한 탐색 경험을 느끼도록 함
  • 현재 포커스된 요소를 시각적으로 구분하여 현재 작업 중인 요소 식별 용이 하도록 함
  • 포커스 트래핑을 구현하여 dialog 내부에서도 자연스런 탐색이 가능하게 함
  • dialog를 열 때 이전의 ref를 기억하여 닫았을 때 해당 위치에서 다시 포커스 요소를 복원하도록 함, 탐색의 흐름 유지
  • 시맨틱 HTML과 ARIA 속성 활용
  • 일반 사용자와 스크린 리더 사용자의 단축키 다르게 설정 하여 사용자 경험 최적화

자세한 코드와 내용:

접근성 개선: 링크  

단축키 다르게 설정: 링크  

툴팁 구현: 링크

키보드로 줌과 패닝 제어: 링크

6. 기타 문제 해결

프로젝트를 진행하며 기록한 문제들의 해결 과정들입니다.

자세한 과정과 코드 내용은 링크를 통해 볼 수 있습니다.

  • swiper 갤러리 tab index 문제 해결: 링크
    • 배경: tab으로 이동 중에 다음 슬라이드의 확대 도구들에 focus가 되어 원치 않는 동작이 발생했습니다
    • 원인: 미리 로딩된 slide 컴포넌트로 focus가 가서 문제가 발생
    • 해결: focus 이탈이 발생하는 컴포넌트에서 isCurrentImage props를 만들어 tabIndex={isCurrentImage ? 0 : -1}로 해결
  • Tailwind css 동적 클래스 문제 해결: 링크
    • 배경: 값을 집어 넣었는데 원하는 size의 원이 나오질 않았습니다
    • 원인: tailwind는 빌드 타임때 생성된 클래스를 바탕으로 동작
    • 해결: 정적 클래스의 사용을 위해 미리 정의된 매핑 객체를 생성해 문제 해결
  • 알 수 없이 렌더링 된 0 해결: 링크
    • 배경: 초기 렌더링 과정에서 알 수 없는 0이 렌더링 되었습니다
    • 원인: 0을 falsy 값으로 0 && {component} 과 같이 썼는데 reactJSX 에서는 이럴 경우 0을 렌더링
    • 해결: 해당 변수에 !!을 붙여 boolean으로 타입 전환
  • react-zoom-pan-pinch: zoom, reset 에러 해결: 링크
    • 배경: 더블클릭으로 zoom과, reset을 반복하는데 정상적으로 작동하지 않는 구간이 발생했습니다
    • 원인: handleZoomChange로는 reset을 한 뒤의 값을 추적하기 어려움, reset이 되었을 때 값이 1이어야 하는데 근사값으로 초기화가 되었음
    • 해결: onZoomStop이라는 속성을 사용하여 값 추적, 근사값을 반영한 기준으로 오류 해결
  • react-zoom-pan-pinch 사진 전환 시 zoom 상태 초기화 하기: 링크
    • 배경: 스와이프를 했을 때 이전의 줌 상황이 유지가 됨
    • 원인: 따로 초기화 코드가 되어있지 않았음
    • 해결: useRef와 useEffect를 사용하여 변경을 추적 후 리셋 함수로 리셋
  • Tab 인덱스가 작동하지 않은 원인: css의 중요성: 링크
    • 배경: tab으로 탐색 도중 어느 순간 focus가 사라져버림
    • 원인: thumbnail 영역이 opacity: 0로 숨겨져 있어 focus가 내부로 들어가버림
    • 해결: 숨기는 css로 display: none을 사용하여 내부로 focus가 들어가지 않도록 수정
  • 과도한 음성 텍스트 제어하기: 적절한 WAI-ARIA 사용의 중요성  : 링크  
    • 배경: 썸네일 버튼을 눌러 영역을 확장했을 때 ' 확장됨 썸네일 갤러리 대화상자 썸네일 보기 닫기 버튼 썸네일 갤러리 버튼 썸네일 보기 닫기 1 슬레쉬 60 이미지 썸네일 목록 목록 버튼 이미지 1' 가 출력 됨
    • 원인: role:dialog 속성으로 내부의 요소가 스캔되어 읽히면서 중복이 발생
    • 해결: 해당 속성을 제거하여 ' 확장된 썸네일 갤러리 영역 닫기 버튼' 으로 딱 필요한 메시지만 전달되도록 함

7. 결과 및 배운 점

개선된 사용자 경험

  • 직관적이고 자연스러운 이미지 탐색 흐름 구현
  • 다양한 사용자 환경(모바일/데스크탑)에서 맞춤화된 사용자 경험 제공
  • 접근성 지원으로 다양한 사용자층 포용

기술적 인사이트

  • 복잡한 사용자 인터랙션을 관리하는 효과적인 상태 설계의 중요성
  • 컴포넌트 분리와 커스텀 훅을 통한 코드 재사용성 향상
  • 공식 홈페이지에서 라이브러리를 탐색하며 원하는 기능 커스텀 역량 상승
  • 사용자 중심 디자인, 특히 복잡한 인터랙션을 단순하고 직관적인 사용자 경험으로 변환하는 과정에서 많은 인사이트를 얻었습니다.
  • 웹 접근성을 최적화 하기 위해 NVDA 스크린 리더를 설치하여 테스트 해보았습니다. 많은 정보를 제공하려다 오히려 사용자를 불편해지게 하는 상황을 겪었고 이 경험을 통해 테스트의 중요성과 뭐든지 적절히 사용해야한다는 것을 깨달았습니다.
  • 이미지 뷰어의 특성상 살짝만 최적화가 되어 잘못되어도 버벅임이 발생했습니다. 이 부분을 해결하기 위해 많은 노력을 하였고 최적화를 깊게 익힐 수 있었습니다.
  • 개발 도중 스크린 리더를 사용하는 하는 사람이 과연 이것을 쓸까의문이 들었지만 방법을 만들어 놓는 것과 만들어 놓지 않는 것은 큰 차이가 있으며, 장애의 경중과 형태가 모두 달라 어떻게 쓰일지 모른다는 생각이 들어 열심히 끝까지 개발 했습니다.

+ Recent posts