FE | 스크롤에 따른 헤더 노출 컨트롤 UX 리팩토링 기술 회고

안녕하세요, 부트텐트 프론트엔드 개발자 코디입니다.
이번 글에서는 부트텐트의 캠프 리스트 페이지에서 진행한 사용자 경험 개선 작업, 그중에서도 “스크롤에 따라 헤더를 숨기고 다시 보여주는 동작”에 관한 리팩토링 과정을 다뤄보려 합니다.
단순히 헤더를 숨기고 다시 보여주는 기능처럼 보이지만, 실제로 구현에 들어가 보니 사용자 흐름과 맥락을 세심하게 고려해야 했고, 반복적인 구조 변경과 상태 설계 리팩토링이 필요했습니다.
이 글에서는 그 과정에서 마주친 문제 상황과 다양한 시도들, 각각의 선택에서 얻은 인사이트, 그리고 최종적으로 도출된 안정적인 구조까지의 여정을 기록했습니다.
프론트엔드 UI 개선에서 “작은 개선 하나가 어떻게 사용자 경험 전체를 바꿀 수 있는지”에 관심 있는 분들에게 이 글이 도움이 되었으면 합니다.
0. 문제 인식: 캠프리스트 페이지의 구조적 한계(너무 두꺼운 상단 고정 UI)
부트텐트의 캠프리스트 페이지는, 다양한 부트캠프 정보를 유저에게 한눈에 비교할 수 있도록 설계한 페이지입니다. 여러 가지의 정보를 한 눈에 비교하고, 본인에게 맞는 부트캠프를 고를 수 있는 구조로 되어 있습니다.
다만, 기능과 정보가 많아지다 보니 상단에 위치한 UI도 점점 두꺼워지는 문제가 생겼습니다.
해당 페이지를 처음 진입해서 스크롤하며 부트캠프 테이블을 둘러볼때면 다음과 같은 4단계의 상단 고정 UI가 연달아 보입니다.
- 메인 헤더 (사이트 전역에서 사용되는 네비게이션)
- 카테고리 필터 바 (웹개발, AI, 데이터분석 등 카테고리 구분)
- 옵션 필터 바 (온라인/오프라인, 비용 등 상세 필터)
- 테이블 헤더 (캠프 정보 리스트 상단 고정 헤더)
이 중 1~3번은 상단에 항상 고정되어 있으며, 4번은 테이블의 thead에 해당하는 부분으로, 이 역시 스크롤을 내리면 position: sticky로 상단에 고정됩니다.

❗️문제는 이 모든 것이 동시에 보일 때 발생합니다
화면 상단이 총 4줄의 sticky 요소로 차지되다 보니, 특히 노트북이나 모바일 환경에서 실제 리스트가 차지하는 영역이 답답해보였습니다.
🧭 개선의 방향을 고민하게 된 이유
유저가 캠프리스트를 스크롤하면서 탐색할 때,
- 정말 항상 메인 헤더가 보여야 할까?
- 필터는 유지하되, 최상단 헤더만 숨긴다면 더 시원한 화면을 줄 수 있지 않을까?
라는 질문이 생겼습니다.
그래서 여러 사이트의 레퍼런스를 참고하면서, 사용자의 스크롤 방향이나 스크롤 위치에 따라 상단 고정 UI의 노출 여부를 동적으로 제어하는 방식을 검토하게 되었습니다.
1. 첫 번째 시도: 스크롤 방향에 따른 헤더 숨김
상단 영역이 너무 두껍게 고정되어 있는 문제를 해결하기 위해, 가장 먼저 시도해본 방식은 스크롤 방향에 따라 메인 헤더만 숨기는 방법이었습니다. 사용자의 탐색 흐름을 살펴보면 보통 다음과 같은 패턴이 있습니다.
- 위로 스크롤할 때 → 다시 상단으로 돌아가거나 필터를 조작하려는 의도
- 아래로 스크롤할 때 → 리스트를 탐색하려는 의도
이 흐름을 고려해서, 스크롤이 아래로 내려갈 때는 메인 헤더를 숨기고, 다시 위로 올릴 때는 헤더를 다시 보여주는 UX를 적용해보았습니다.

🛠 useScrollDirection 훅 작성
스크롤 방향을 감지하기 위해 커스텀 훅인 useScrollDirection을 만들었습니다.
이 훅은 scrollY 값을 기준으로 이전 위치와 현재 위치를 비교하여 스크롤 방향(up/down) 을 판단해 상태를 업데이트합니다.
import { debounceFunc } from "@/lib/utils";
import { useEffect, useRef, useState } from "react";
export function useScrollDirection(threshold = 50, debounceDelay = 100) {
const [isScrollingUp, setIsScrollingUp] = useState(false);
const [isScrollingDown, setIsScrollingDown] = useState(false);
const lastScrollY = useRef(0);
const handleScroll = debounceFunc(() => {
const currentScrollY = window.scrollY;
if (Math.abs(currentScrollY - lastScrollY.current) > threshold) {
if (currentScrollY > lastScrollY.current) {
setIsScrollingDown(true);
setIsScrollingUp(false);
} else {
setIsScrollingUp(true);
setIsScrollingDown(false);
}
lastScrollY.current = currentScrollY;
}
}, debounceDelay);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return { isScrollingUp, isScrollingDown };
}
위의 훅과 framer-motion을 사용하면 아래와 같이 헤더에 스크롤 방향에 따른 애니메이션을 적용할 수 있습니다.
<motion.header
animate={{ y: isScrollingDown ? -headerHeight : 0 }}
transition={{ duration: 0.2 }}
>
<MainHeader />
</motion.header>
✅ 기대했던 UX
- 유저가 리스트를 탐색하면서 아래로 스크롤하면 메인 헤더가 사라져서 화면이 더 넓게 보이고 시원한 느낌을 줄 수 있을 것이라고 생각했습니다.
- 다시 위로 스크롤하면 헤더가 나타나므로, 필터나 검색으로 빠르게 복귀할 수 있는 구조도 유지했습니다.
❌ 그러나 실제 적용 결과는 달랐습니다
생각보다 사용자 경험이 불안정했습니다.
- 조금만 스크롤해도 헤더가 사라졌다 나타남. 스크롤에 헤더가 계속 들락날락하니 오히려 더 정신없는 것 같기도.
- 집중이 분산되는 느낌. 콘텐츠를 스크롤하다가, 예상치 못하게 헤더가 튀어나오거나 사라지는 느낌이 거슬림
- 모바일에서는 더 어색함. 손가락 스크롤로는 방향 감지가 잦아, 헤더가 안정적으로 고정되지 못함.
특히 필터 영역과 테이블 헤더는 그대로 고정된 상태였기 때문에, 메인 헤더만 계속 들쑥날쑥하는 것이 더 어색하게 느껴졌습니다. 만족스럽지는 못했습니다.
🔄 다음 시도로 넘어가기로 함
그래서 고민을 다시 정리해보았습니다.
- 메인 헤더는 꼭 항상 보여야 하는 요소는 아니다.
- 그렇다면, 스크롤 방향이 아니라 스크롤 위치를 기준으로 감추는 방식은 어떨까?
2. 두 번째 시도: 스크롤 위치를 기준으로 헤더 숨김
useScrollDirection을 이용한 시도는 기능적으로는 잘 동작했지만, 실제 사용자 입장에서 봤을 때는 다소 불안정한 경험을 주었습니다. 스크롤 방향이 감지될 때마다 헤더가 들락날락하다 보니, 오히려 더 눈에 거슬리는 결과가 되었기 때문입니다. 그래서 방향을 조금 바꿔보기로 했습니다.
“스크롤이 일정 거리 이상 내려가면, 그때만 헤더를 감추는 건 어떨까?”
이 방식은 ‘사용자가 리스트를 본격적으로 탐색하고 있다’는 명확한 신호를 기준으로 헤더를 숨기기 때문에, 이전 방식보다 훨씬 더 안정적인 UX를 제공할 수 있을 것이라고 판단했습니다.

🛠 useScrollThreshold 훅 작성
이를 구현하기 위해 useScrollThreshold라는 커스텀 훅을 만들었습니다.
이 훅은 window.scrollY가 특정 임계값(threshold)을 넘었는지 여부를 판단해서 true 또는 false 값을 반환해줍니다.
"use client";
import { useEffect, useState } from "react";
export type ScrollDirection = "down" | "up";
export const useScrollThreshold = (threshold: number, direction: ScrollDirection = "down"): boolean => {
const [isPastThreshold, setIsPastThreshold] = useState(false);
useEffect(() => {
let ticking = false;
const update = () => {
const scrollY = window.scrollY;
if (direction === "down") {
setIsPastThreshold(scrollY >= threshold);
} else {
setIsPastThreshold(scrollY <= threshold);
}
ticking = false;
};
const handleScroll = () => {
if (!ticking) {
window.requestAnimationFrame(update);
ticking = true;
}
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, [threshold, direction]);
return isPastThreshold;
};
스크롤이 threshold 값을 넘으면 true, 아니면 false를 반환하는 단순한 훅이지만, 기존의 방향 기반 제어보다 UX 측면에서 훨씬 자연스러운 전환을 만들어낼 수 있었습니다.
✅ 실제 적용
예를 들어 threshold 값을 500으로 설정하고, 메인 헤더 컴포넌트에 아래와 같이 적용하면
const isPastThreshold = useScrollThreshold(500, "down");
<motion.header
animate={{ y: isPastThreshold ? -headerHeight : 0 }}
transition={{ duration: 0.2 }}
>
<MainHeader />
</motion.header>
사용자가 스크롤을 500px 이상 내렸을 때만 헤더가 위로 사라지고, 그 이전까지는 고정된 채로 자연스럽게 유지됩니다.
👍 개선된 점
- 헤더가 자주 들쑥날쑥하지 않음
- 사용자가 스크롤을 조금만 움직일 때는 헤더가 그대로 유지되어 안정감 있음
- 탐색을 시작한 이후에는 시야가 넓어져 콘텐츠 몰입도가 높아짐
❌ 그러나 새롭게 발견된 문제
이번 개선은 전보다 확실히 좋아졌지만, 스크롤을 너무 많이 내린 이후에는 헤더를 다시 보기 어렵다는 문제가 생겼습니다. 물론 페이지에 플로팅버튼으로 scrollToTop 버튼이 있긴 했지만, 단순히 필터를 다시 보려는 목적이라면 스크롤 최상단까지 이동해야 한다는 점은 큰 UX 부담이었습니다.
🔁 다음 개선 아이디어
이 문제를 해결하기 위해 다음과 같은 고민을 하게 됩니다.
- 사용자가 필요할 때 헤더를 다시 쉽게 볼 수 있는 방법은 없을까?
- 예를 들어 필터 바에 마우스를 올리면 다시 헤더가 잠깐 나타난다거나…
- 그럼 호버의 개념이 없는 모바일은 어떻게 하면 좋을까?
이러한 시나리오를 떠올리며, 수동으로 헤더를 보여줄 수 있는 구조를 도입하게 되었고, Context 대신 Zustand로 상태를 관리하도록 구조를 다시 설계하게 됩니다.
3. 세 번째 시도: 사용자 행동 기반의 헤더 복원 (호버)
스크롤 위치를 기준으로 헤더를 숨기는 방식은 이전보다 훨씬 안정적이었지만, 또 다른 문제를 드러냈습니다. 사용자가 리스트를 탐색하다가 다시 필터를 조작하고 싶을 때, 스크롤을 최상단까지 올려야만 헤더를 다시 볼 수 있는 불편함이 있었습니다. 이러한 한계를 극복하기 위해 사용자 행동 기반으로 헤더를 복원하는 방식은 어떨지 고민하게되었습니다.
“사용자가 헤더를 보고 싶을 때, 더 쉽게 보여줄 수 있는 방법은 없을까?”
사용자 행동을 헤더의 노출 여부를 판단하는 기준으로 삼아, 수동 제어(사용자의 명확한 행동 기반)와 자동 제어(스크롤 위치 기반)를 병행하는 방식으로 구조를 재설계하기로 했습니다.
🧠 상태 관리를 어떻게 할 것인가?
이제는 단순히 스크롤 위치나 방향 값만 구독하는 방식에서 나아가, 사용자의 행동에 따라 직접적으로 상태를 변경할 필요가 생겼습니다. 예를 들어 필터 바에 마우스를 올렸을 때 메인 헤더가 다시 나타나도록 해야 했습니다.
처음에는 React의 useContext를 이용하여 스크롤 기준값과 헤더 상태를 관리했습니다.
import React, { createContext, useContext, ReactNode } from "react";
import { useScrollThreshold } from "@/hooks/dom";
import { HEADER_SCROLL_THRESHOLD } from "@/styles";
const ScrollHeaderContext = createContext<{ isPastThreshold: boolean, isScrollingDown : boolean } | undefined>(undefined);
export const ScrollHeaderProvider = ({ children }: { children: ReactNode }) => {
const isPastThreshold = useScrollThreshold(HEADER_SCROLL_THRESHOLD, "down");
const { isScrollingDown } = useScrollDirection(200, 0);
return (
<ScrollHeaderContext.Provider value={{ isPastThreshold, isScrollingDown }}>
{children}
</ScrollHeaderContext.Provider>
);
};
export const useScrollHeader = () => {
const context = useContext(ScrollHeaderContext);
if (!context) return { isPastThreshold:false, isScrollingDown: false }
return context;
};
→ 하지만 이 방식은 곧 두 가지 큰 문제를 드러냈습니다.
🚩 문제 1: 필터 바의 액션을 헤더에 전달하기 어려움
필터 바는 특정 페이지 내부에 존재하지만, 메인 헤더는 전체 앱의 루트 레이아웃에 존재했습니다. 따라서 필터 바에서 발생한 사용자 액션(마우스 호버)을 메인 헤더가 있는 레이아웃까지 전달하려면 매우 번거로운 구조가 필요했습니다.
결국 필터 바에서 일어난 일이 루트에 있는 헤더에 직접 영향을 줘야 하는 상황이므로, useContext를 통한 값 전달은 매우 비효율적이고 복잡한 구조를 초래했습니다. 따라서 상태의 전역 관리가 더욱 적절했습니다.
🚩 문제 2: 상태를 직접적으로 조작해야 하는 상황 발생
초기에는 단순히 훅이 반환하는 값(isPastThreshold, isScrollingDown)을 구독하는 방식이었지만, 사용자 행동에 따라 상태를 명확하게 조작할 필요가 생겼습니다. 예를 들어 사용자가 필터 바에 호버할 때 헤더를 강제로 나타나게 하거나 숨겨야 하는 등 직접 상태를 변경해야 하는 요구사항이 생긴 것입니다.
useContext는 본래 상태 자체를 갖지 않고, props drilling을 피하기 위한 값 주입 도구로 설계되었습니다. 따라서 상태 변경 로직(setter)을 함께 관리하려면, 상태 전체를 별도의 외부 상태 관리 로직에 두고 이를 Context로 넘기는 방식이 필요했습니다. 그러나 이 방식은 다음과 같은 구조적인 문제를 낳았습니다.
- 상태 변경 로직이 한 곳에 집중되어 관리가 어려움
- Context.Provider의 위치 선정이 까다로움
- setter 함수를 Context로 전달하면 불필요한 결합도를 높임
결국 앱 전역에서 상태를 읽고 쓰는 작업을 관리하기에는 useContext가 적합하지 않다는 결론에 이르렀습니다.
🛠 Zustand를 활용한 상태 관리로 전환
이러한 이유로 Zustand를 선택하게 되었습니다. Zustand는 Provider가 필요 없으며, 컴포넌트 위치에 상관없이 자유롭게 상태를 공유할 수 있는 강력한 상태 관리 라이브러리입니다. Zustand를 사용하여 상태를 관리하면서, 상태와 그 변경 로직을 깔끔하게 응집할 수 있었습니다.
export const useHeaderVisibilityStore = create((set) => ({
isHeaderVisible: true,
setIsHeaderVisible: (value) => set({ isHeaderVisible: value }),
}));
Zustand를 통해 얻은 이점은 다음과 같습니다.
🧩 사용자 행동에 따른 헤더 상태 관리: 커스텀 훅으로 집중화
다양한 트리거 조건(스크롤 방향, 위치, 모바일 환경, 사용자 호버 액션)을 한 곳에서 통합 관리하기 위해, useScrollHeaderVisibility라는 커스텀 훅을 작성했습니다.
export const useScrollHeaderVisibility = () => {
const { mobileView } = useResponsive();
const isMobileView = mobileView();
const pathname = usePathname();
const isHeaderControlledPage = pathname === "/camps";
const detected = useScrollThreshold(HEADER_SCROLL_THRESHOLD, "down");
const { isScrollingDown } = useScrollDirection(200, 0);
const triggered = isMobileView ? isScrollingDown : detected;
const { setIsHeaderVisible, isHeaderVisible } = useHeaderVisibilityStore();
useEffect(() => {
setIsHeaderVisible(!triggered);
}, [triggered, setIsHeaderVisible]);
const handleHover = (e: React.MouseEvent<HTMLDivElement>) => {
if (isMobileView) return;
if (e.type === "mouseover") setIsHeaderVisible(true);
else if (e.type === "mouseleave" && triggered) setIsHeaderVisible(false);
};
return { isHeaderVisible, handleHover, isHeaderControlledPage };
};
이 커스텀 훅을 통해 모든 헤더의 노출 관련 로직을 한곳에 집중시켰고, UI 컴포넌트는 단순히 이 훅에서 제공하는 상태를 구독하여 명확하고 깔끔한 구조를 유지할 수 있었습니다. 이로 인해 유지보수와 확장성이 크게 향상되었습니다.
4. 네 번째 시도: Peek UI 도입을 통한 명확한 의도 전달
세 번째 시도에서 사용자 행동을 기반으로 헤더를 복원하는 방식으로 개선하면서 사용성은 전보다 좋아졌습니다. 그러나 실제 적용 이후 예상치 못한 UX 문제가 또 나타났습니다.
❗️호버(Hover) 방식의 예상치 못한 문제
스크롤을 내린 상태에서 사용자가 필터 바에 마우스를 올리면 헤더가 다시 나타나도록 구현했습니다. 사용자가 필터를 조작할 확률이 높다고 생각했기 때문입니다. 하지만 사용자의 실제 반응은 조금 달랐습니다.
이러한 문제가 발생한 이유는 ‘호버’라는 액션이 사용자가 필터를 클릭하려는 것인지, 헤더를 보고 싶어서인지를 명확히 구분할 수 없기 때문이었습니다. 즉, 사용자의 의도를 정확히 파악할 수 없는 근본적인 한계였습니다.
🧭 개선 방향 고민하기
그래서 다시 한 번 UX 측면에서 근본적인 고민을 하게 되었습니다.
- 사용자가 헤더를 다시 보고 싶어 하는 의도를 더 명확하게 파악할 수 있는 방법은 없을까?
- 헤더를 완전히 숨기지 않고 축소된 형태로 남겨둔 뒤, 사용자가 명시적으로 클릭할 때만 복원되는 방식은 어떨까?
이런 질문들을 바탕으로 다양한 디자인 레퍼런스를 조사했고, 결국 Peek UI라는 개념을 도입하기로 결정했습니다.
🎨 Peek UI 디자인 패턴 소개
Peek UI란 특정 UI 요소를 완전히 숨기지 않고, 최소한의 형태로 접어서 사용자가 명시적으로 클릭하면 원래 크기로 다시 확장되는 UX 디자인 패턴입니다.
대표적인 예로 모바일 브라우저를 생각하면 쉽게 이해할 수 있습니다. 페이지를 아래로 스크롤하면 상단 주소 표시줄이 작게 축소되며 접히고, 이 접힌 상태를 탭하면 다시 원래 크기로 펼쳐지는 방식입니다.
Peek UI의 장점은 다음과 같습니다.
- UI 요소가 완전히 사라지지 않고 축소된 형태로 존재하여 사용자가 쉽게 인지 가능
- 콘텐츠 영역 확보가 가능하면서도 사용자가 원할 때 명확하게 UI를 확장할 수 있음
- 호버와 달리 클릭 또는 탭과 같은 명시적인 액션을 통해 의도를 명확히 파악할 수 있음
🛠 Peek UI를 적용한 헤더 설계
Peek UI를 캠프 리스트 페이지에 적용하기 위한 계획은 다음과 같았습니다.
- 사용자가 페이지를 아래로 스크롤하면 헤더가 완전히 숨겨지지 않고 화면 상단에 최소한의 형태로 축소됨.
- 이 축소된 헤더 영역에 작은 화살표(▼) 아이콘을 추가해 사용자가 헤더의 존재를 쉽게 인지하도록 함.
- 사용자가 축소된 영역을 클릭하면 다시 헤더가 원래의 크기로 복원되는 구조.
이를 구현한 코드의 예시는 아래와 같습니다.
import { useHeaderVisibilityStore } from "@/zustand/useHeaderVisibilityStore";
const HeaderPeekBar = () => {
const { setIsHeaderVisible } = useHeaderVisibilityStore();
return (
<div className="header-peek-bar" onClick={() => setIsHeaderVisible(true)}>
<span className="peek-icon">▼</span>
</div>
);
};
최소 높이로 스타일링한 Peek Bar를 통해 사용자가 쉽게 클릭하여 헤더를 다시 볼 수 있게 했습니다.


❗️ Peek UI 도입 이후 발견된 한계
그러나 Peek UI를 실제로 적용해본 후, 생각지 못했던 문제가 또 다시 발견되었습니다.
초기에는 메인 헤더와 카테고리 필터 바를 모두 Peek UI로 축소하려고 했습니다. 하지만 실제로 구현하고 사용성을 평가해보니, 카테고리 필터 바는 리스트 탐색 중 사용 빈도가 매우 높아 반드시 항상 노출되어야 하는 요소였습니다. 따라서 카테고리 필터 바까지 함께 축소되면 사용성이 크게 저하되었습니다.
결국 메인 헤더만 Peek UI로 축소하는 방식으로 방향을 수정했는데, 이번엔 Peek Bar 자체도 어느 정도의 높이를 차지하여 콘텐츠 영역이 충분히 넓어지는 효과를 얻지 못했습니다. 메인 헤더 한 줄만 축소해서는 확보할 수 있는 추가 공간이 매우 적었기 때문입니다.
결과적으로 Peek UI 방식은 화면 공간 확보라는 본래 목적을 달성하는 데 크게 기여하지 못했습니다.
🔄 다시 고민
Peek UI 적용의 한계를 경험하고 다시금 UX 구조에 대한 근본적인 고민을 시작했습니다.
- 결국 화면 공간을 충분히 확보하려면 상단 Sticky 영역 자체를 줄이는 근본적인 접근이 필요하지 않을까?
- 필수적인 카테고리 필터와 옵션 필터 영역을 아예 통합하면 어떨까?
이러한 고민 끝에 Peek UI는 도입하지 않기로 최종 결정하고, 대신 카테고리 필터를 옵션 필터 바에 통합하는 방식을 도입하기로 했습니다. 이를 통해 헤더 구조 자체를 더 단순화하고, 메인 헤더를 스크롤 방향에 따라 완전히 숨기고 노출시키는 심플한 접근 방식으로 최종 UX를 개선하기로 했습니다.
이렇게 네 번째 시도에서도 여전히 UX의 명확성을 찾기 위한 다양한 고민과 새로운 방향 전환이 이어지게 되었습니다.
5. 다섯 번째(최종) 시도: Peek UI를 포기하고, 필터 통합과 단순한 헤더 제어로의 최종 결정
네 번째 시도에서 Peek UI를 도입해 사용자의 명확한 의도를 파악하는 방식으로 접근했습니다. 하지만 실제로 구현해 사용성 평가를 진행해보니, Peek UI는 오히려 화면 공간 확보라는 본연의 목적을 제대로 달성하지 못했습니다. 결국 최종적으로 Peek UI 도입을 포기하고, 근본적인 구조 개선으로 다시 방향을 전환하게 되었습니다.
🔁 방향 전환: 근본적인 상단 UI 구조의 단순화
이러한 시행착오를 겪은 후, 더 근본적인 구조 변화를 고민하게 되었습니다. 결국 화면 상단의 Sticky 영역 자체를 근본적으로 줄이는 것이 최선의 방법이라는 결론에 도달했습니다.
이를 위해 다음과 같은 방향으로 최종 개선을 결정했습니다.
- 메인 헤더만을 스크롤 방향에 따라 완전히 숨기거나 보이게 함
- 카테고리 필터 바는 옵션 필터 바와 완전히 하나의 영역으로 통합하여 항상 보이게 유지
- 테이블 헤더 역시 기존대로 유지하여 항상 보임
이러한 방식으로 구조를 단순화하면서도, 필수적인 필터의 접근성을 보장하고 화면 공간도 최대한 확보할 수 있었습니다.
🧩 카테고리 필터와 프로그램 과정 필터의 통합
기존 캠프 리스트 페이지는 다음과 같은 필터 구조로 이루어져 있었습니다.
이 구조의 가장 큰 문제점은 카테고리와 프로그램 과정 필터가 분리되어 있어 사용자가 필터를 두 번에 걸쳐 선택해야 한다는 점이었습니다. 또한 이렇게 필터가 두 줄로 나뉘어 있어 공간을 불필요하게 많이 차지했습니다.
🚀 최종 개선된 필터 구조: 2중 드롭다운으로 통합
이 문제를 해결하기 위해 카테고리 필터와 프로그램 과정 필터를 하나로 합친 2중 드롭다운 구조를 도입했습니다.
예를 들어, 사용자가 첫 번째 드롭다운에서 웹개발이라는 카테고리를 선택하면, 바로 두 번째 드롭다운에 프론트엔드, 백엔드, 풀스택, 퍼블리셔 등의 웹개발 카테고리 하위 과정만 선택 가능한 옵션으로 제공됩니다.
즉, 사용자는 이제 카테고리와 그 하위 세부 과정을 한 번의 선택 과정으로 해결할 수 있게 된 것입니다.
이를 코드로 구현하면 다음과 같은 구조가 됩니다.
// 2중 드롭다운 구조 예시
<DropdownMenu>
<DropdownMenuTrigger>카테고리 선택</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuSub>
<DropdownMenuSubTrigger>웹개발</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>프론트엔드</DropdownMenuItem>
<DropdownMenuItem>백엔드</DropdownMenuItem>
<DropdownMenuItem>풀스택</DropdownMenuItem>
<DropdownMenuItem>퍼블리셔</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>데이터 AI</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>데이터분석</DropdownMenuItem>
<DropdownMenuItem>데이터엔지니어링</DropdownMenuItem>
<DropdownMenuItem>데이터사이언스</DropdownMenuItem>
<DropdownMenuItem>AI/ML</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
{/* 기타 카테고리 추가 */}
</DropdownMenuContent>
</DropdownMenu>
이러한 변경은 UX에 있어 다음과 같은 큰 개선을 가져왔습니다.
🎬 최종적으로 결정된 상단 영역 구조
이 최종 구조의 UX 흐름은 다음과 같이 정리됩니다.

최종적으로 채택한 이 구조는 이전의 여러 시도들과 비교해 가장 심플하면서도 UX 측면에서 균형 잡힌 결과를 만들어낼 수 있었습니다. 동시에 화면 공간을 최대한 확보하여 사용자에게 더 나은 콘텐츠 탐색 경험을 제공할 수 있게 되었습니다.
6. 회고 및 마무리: 사용자 경험을 위한 설계란 무엇인가?
처음 이 작업을 시작했을 때는 단순히 “헤더가 너무 크니까 스크롤을 내릴 때 숨겨보자”는 간단한 목표였습니다. 하지만 실제 작업에 들어가면서 생각보다 훨씬 더 복잡한 UX 맥락과 기술적 판단들이 얽혀 있었습니다. 구현 과정에서 나타난 수많은 문제와 시도, 그 속에서 고민한 내용들을 정리해보니 결국 하나의 중요한 질문으로 수렴되었습니다.
“좋은 사용자 경험이란 과연 무엇인가?”
이번 작업에서 얻은 인사이트와 깨달음을 공유하며 회고를 마무리하고자 합니다.
🎯 얻은 기술적 인사이트
💡 결국 사용자의 의도와 맥락
작업 초기에 구현한 헤더는 사용자의 스크롤 방향에 따라 숨기고 보이도록 했지만, 실제로는 사용자에게 “불안정하고 불편한 경험”을 주었습니다. 다음으로 적용한 스크롤 위치 기반의 제어는 개선되었으나, 헤더가 숨겨졌을 때 다시 보는 과정이 너무 번거롭다는 문제를 발견했습니다.
이러한 시행착오를 겪으면서 알게 된 가장 큰 교훈은, “사용자가 UI를 인지하고 사용하는 맥락과 의도를 정확히 반영해야 한다”는 것입니다.
처음 시도했던 Peek UI는 사용자에게 헤더의 존재를 인식하게 하면서 필요할 때 쉽게 복원할 수 있도록 돕는 좋은 아이디어였지만, 실제로는 기대했던 만큼의 효과를 얻지 못했습니다. 필터의 중요도와 사용 빈도를 충분히 고려하지 않은 채 단순히 화면 영역 확보라는 목표만 추구했기 때문입니다.
결국, 실제 사용자의 맥락을 충분히 반영하여 카테고리 필터를 옵션 필터 바로 통합했고, 메인 헤더만 스크롤 방향에 따라 깔끔하게 숨기거나 노출시키는 방향으로 설계를 단순화했습니다. 이는 사용자 입장에서 더 직관적이고 편리한 구조가 되었습니다.
✍🏻 마무리하며
“단순히 보여줄지 말지의 문제가 아니라, 언제, 왜, 어떻게 보여줄지를 설계하는 것이 진짜 사용자 경험이다.”
UI의 작은 부분 하나라도 사용자의 입장과 맥락을 고려하지 않고는 진정한 의미의 UX 개선이 될 수 없다는 것을 깊이 깨닫는 시간이었습니다. 사용자가 필요할 때 원하는 요소가 자연스럽게 나타나고, 불필요할 때는 방해하지 않고 사라지는 작은 경험 뒤에는 수많은 고민과 구조적인 결정들이 존재한다는 점을 배웠습니다.
앞으로의 작업에서도 계속해서 “사용자의 맥락을 깊이 이해하는 태도”와 “상태를 명확히 설계하고 분리하는 습관”을 유지하며 더 나은 사용자 경험을 만들어 가겠습니다.
긴 글 읽어주셔서 감사합니다! 앞으로도 좋은 사례로 다시 찾아뵙겠습니다. 😊
고민이 있으시거나 대화하고싶으신 분께서는 아래 1:1 오픈채팅 링크로 놀러오세요!
편하게 오셔서 부트캠프고민, 취업고민, 인생고민 같이 나눠봐요.