[UI/UX 개선 2부] 사소하지만 위대한 디테일: 체감 성능, 접근성, 그리고 스크롤 위치 복원
1부에서는 메인 페이지의 확장성 문제를 해결하고, 페이지네이션과 무한 스크롤이라는 튼튼한 '뼈대'와 '근육'을 만들었습니다. 기능적으로는 완벽했지만, 진정한 '고품질' UX는 눈에 보이지 않는 디테일에서 완성된다고 믿습니다.
이 글은 그 믿음을 바탕으로, 사용자가 "왜 인지는 모르겠지만, 이 블로그는 참 편안하고 전문적이다"라고 느끼게 만드는 세 가지 핵심 디테일 - 체감 성능, 웹 접근성, 그리고 스크롤 위치 복원 - 을 구현한 과정을 담은 과정을 기록해 보았습니다.
Part 1. 체감 성능 최적화: 0.2초의 마법, Pre-loading
무한 스크롤은 훌륭하지만, 사용자가 스크롤을 멈춘 순간 로딩 스피너를 보게 된다면 약간의 '기다림'을 인지하게 됩니다. 저는 이 미세한 기다림마저 없애고 싶었습니다.
문제: 현재 로직은 페이지의 맨 끝에 도달해야만 다음 페이지 로딩을 시작합니다. 해결책: 사용자가 끝에 도달할 것을 미리 예측하고, 한발 앞서 로딩을 시작하는 것입니다.
Intersection Observer API에는 이를 위한 rootMargin이라는 강력한 옵션이 있습니다. rootMargin은 실제 뷰포트(화면) 경계선 바깥쪽에 가상의 '여백'을 만들어, 관찰 대상이 이 가상의 경계선에 닿았을 때를 '보이는 것'으로 간주하게 합니다.

설명:
rootMargin: '200px'설정은 화면 하단보다 200px 더 아래에 '감지선'을 만드는 것과 같다. 사용자가 스크롤을 내릴 때, 페이지 끝에 있는 '트리거' 요소가 이 감지선에 닿는 순간, 아직 화면에 보이지 않음에도 불구하고 다음 페이지 로딩이 시작된다.
// 파일 위치: apps/frontend/src/components/PostList.tsx const { setTarget, entry } = useIntersectionObserver({ // 뷰포트 하단에서 200px 떨어진 지점을 감지선으로 설정 rootMargin: '200px', threshold: 0.1, });
이 단 한 줄의 변경으로, 사용자가 스크롤을 멈추고 로딩 스피너를 볼 확률이 극적으로 줄어들었습니다. 데이터는 사용자가 눈치채지 못하는 사이에 미리 로드되고, 스크롤은 마치 처음부터 모든 콘텐츠가 있었던 것처럼 부드럽게 이어집니다. 이것이 바로 실제 성능(Real Performance)이 아닌, 사용자가 느끼는 **체감 성능(Perceived Performance)**을 최적화하는 기술의 힘입니다.
Part 2. 모두를 위한 블로그: 웹 접근성(ARIA) 강화
로딩 스피너는 시각적으로는 훌륭한 피드백이지만, 스크린 리더를 사용하는 시각장애인 사용자에게는 아무런 의미도 전달하지 못합니다. "모두를 위한 블로그"를 만들기 위해, 저는 동적으로 콘텐츠가 로드되는 영역에 ARIA(Accessible Rich Internet Applications) 속성을 추가하여 보이지 않는 정보를 전달하기로 했습니다.
1. 로딩 상태 알리기 (aria-busy)
게시물 목록을 감싸는 그리드 div에 aria-busy 속성을 추가했습니다. 이 속성은 스크린 리더에게 "이 영역의 콘텐츠가 현재 업데이트 중이니, 잠시 기다려주세요"라고 알려주는 역할을 합니다.
// 파일 위치: apps/frontend/src/components/PostList.tsx <div className="grid ..." // isRefreshing(로딩 중) 상태일 때 'true'가 됨 aria-busy={isRefreshing} > {/* PostCard 목록 */} </div>
2. 상태 변경 알리기 (role="status")
로딩 스피너가 나타나는 영역에는 role="status" 속성을 부여했습니다. 스크린 리더는 이 역할이 부여된 영역의 내용이 변경되면, 그 내용을 사용자에게 음성으로 알려줍니다.
여기에 시각적으로는 보이지 않지만 스크린 리더는 읽을 수 있는 텍스트(sr-only)를 함께 추가하여, "게시물을 불러오는 중"이라는 명확한 음성 피드백을 제공하도록 했습니다.

설명: 로딩 스피너와 함께, 스크린 리더 사용자만을 위한 숨겨진 텍스트를 제공한다.
role="status"는 이 영역의 변화를 보조 기술이 감지하고 사용자에게 알려주도록 하는 약속이다.
// 파일 위치: apps/frontend/src/components/PostList.tsx <div ref={setTarget} className="..." // 이 영역이 상태 메시지를 담고 있음을 선언 role="status" aria-live="polite" > {!isReachingEnd && isRefreshing && ( <> <Spinner /> {/* 스크린 리더 사용자에게만 들리는 텍스트 */} <span className="sr-only">게시물을 불러오는 중</span> </> )} </div>
이 작은 변화들을 통해, Deep Dive! 블로그는 이제 시각적 정보에 의존하지 않는 사용자에게도 동등한 수준의 정보와 경험을 제공하는, 더 포용적인 공간으로 발전했습니다.
Part 3. 탐색의 연속성: 스크롤 위치 복원
무한 스크롤 페이지의 가장 큰 단점 중 하나는, 다른 페이지로 이동했다가 '뒤로 가기'로 돌아왔을 때 모든 맥락을 잃고 페이지 맨 위로 돌아가 버린다는 것입니다. 이 문제를 해결하는 것은 사용자 경험의 완성도를 결정하는 마지막 관문이었습니다.
해결 전략: 브라우저의 sessionStorage를 활용하여, 사용자가 페이지를 떠나기 직전의 상태를 기억해두었다가, 다시 돌아왔을 때 그 상태를 복원해주기로 했습니다.
이를 위해 useScrollRestoration이라는 새로운 커스텀 훅을 설계했습니다.

설명:
useScrollRestoration훅의 핵심.beforeunload이벤트(페이지를 떠날 때)를 감지하여 현재 스크롤 위치와 로드된 페이지 수를sessionStorage에 저장하고,popstate이벤트('뒤로 가기'로 돌아올 때)를 감지하여 복원을 준비한다.
// 파일 위치: apps/frontend/src/hooks/useScrollRestoration.ts // 1. 페이지 이동 시 스크롤 위치와 size 저장 useEffect(() => { const handleBeforeUnload = () => { const scrollState = { scrollY: window.scrollY, size: size }; sessionStorage.setItem(storageKey, JSON.stringify(scrollState)); }; window.addEventListener('beforeunload', handleBeforeUnload); // ... }, [storageKey, size]); // 2. '뒤로 가기'로 돌아왔는지 확인하고, 복원 준비 useEffect(() => { const handlePopState = () => { shouldRestore.current = true; }; window.addEventListener('popstate', handlePopState); // ... }, []); // 3. 실제 복원 로직 useEffect(() => { if (shouldRestore.current && isReady) { const savedState = JSON.parse(sessionStorage.getItem(storageKey) || '{}'); if (savedState.scrollY) { window.scrollTo({ top: savedState.scrollY, behavior: 'auto' }); } shouldRestore.current = false; } }, [isReady, storageKey]);
PostList 컴포넌트에서는 이 훅을 단 한 줄로 호출하기만 하면 됩니다.
// 파일 위치: apps/frontend/src/components/PostList.tsx // 데이터 로딩이 완료되었을 때(!isRefreshing)만 복원을 활성화합니다. useScrollRestoration('main-scroll-position', size, !isRefreshing);
이 기능의 추가로, 사용자는 이제 마음껏 게시물을 탐색하다가 상세 페이지로 들어갔다 나와도, 자신이 어디까지 읽었는지 잃어버리는 불편함 없이 탐색의 여정을 부드럽게 이어갈 수 있게 되었습니다.
최종 회고
눈에 띄는 화려한 기능을 추가한 것은 아니었습니다. 하지만 체감 성능, 접근성, 스크롤 복원과 같은 보이지 않는 디테일을 개선하는 과정은, 'UX'의 본질에 대해 다시 한번 깊이 생각하게 만드는 계기가 되었습니다.
결국 사용자는 우리가 얼마나 화려한 기술을 사용했는지 보다, "이 사이트가 얼마나 빠르고, 편안하고, 나를 배려하는가" 를 통해 서비스의 가치를 판단합니다. 이 작지만 위대한 디테일들이 쌓여, Deep Dive! 블로그가 사용자의 신뢰를 얻는 견고한 토대가 되기를 바랍니다.
