New TBW(TEAMBUTFIT Web) 프로젝트 회고
작성자 : 오예환 | 작성일 : 2025-11-23 | 수정일 : 2025-11-23
들어가며
팀버핏은 근력·지구력·유연성을 통합적으로 단련하는 팀워크 기반 팀 트레이닝입니다. 요일별 체계적 프로그램, 잘 훈련된 코치들, 고객 수준별 및 성향별로 제공되는 맞춤형 케어, 끈끈한 커뮤니티를 제공합니다.
2025년 하반기, 경기 침체로 인한 신규 유입 감소 문제를 해결하기 위해 공식 웹사이트 리뉴얼 프로젝트를 진행했습니다. 이 글에서는 프로젝트를 진행하며 배운 점과 마주한 기술적 문제들을 공유합니다.
- 프로젝트 기간: 2025년 10월 - 11월
- 기술 스택: Next.js, TypeScript, Styled-components
- 팀버핏 웹사이트: https://teambutfit.com/
프로젝트 배경
왜 리뉴얼이 필요했는가?
2025년 하반기, 경기 불황으로 인해 전반적인 소비 심리가 위축되면서 팀버핏 웹사이트의 신규 유입이 감소하는 추세를 보였습니다. 특히 기존 팀버핏 웹사이트는 다음과 같은 문제점을 가지고 있었습니다:
- 불친절한 사용자 경험: 처음 방문한 고객이 팀버핏 서비스를 이해하기 어려운 구조
- 복잡한 결제 플로우: 체험권 구매까지 이어지는 과정이 직관적이지 않음
- 모호한 서비스 설명: 팀버핏이 어떻게 진행되는지에 대한 친절한 안내 부족
리뉴얼 목표
이번 프로젝트는 다음 두 가지 핵심 목표에 집중했습니다:
- 신규 유입 고객을 위한 친절한 UX: 팀버핏 서비스가 어떻게 진행되는지 단계별로 명확하게 설명
- 간편한 체험권 구매 플로우: 방문자가 최소한의 클릭으로 체험권을 구매할 수 있도록 개선
마케팅 추적 시스템 개선
마케팅팀의 니즈
팀버핏은 이미 GA를 통해 유입 경로를 추적하고 있었다. 하지만 마케팅팀에서는 GA 데이터를 신뢰하기 어렵다는 피드백을 주셨다.
**마케팅팀** : "GA로는 실제로 회원가입한 유저가 어디서 유입됐는지 정확히 알기 어려워요. DB에 직접 저장할 수 있을까요?"
구체적으로 알고 싶었던 내용은 다음과 같았다:
- 사용자가 어떤 홍보 채널(네이버 블로그, 인스타그램 광고 등)을 통해 유입되었는가?
- 웹사이트 내에서 어떤 섹션에서 체험권을 구매했는가?
- 어떤 경로로 회원가입을 완료했는가?
GA 데이터의 한계는 명확했다. GA는 세션 기반으로 데이터를 수집하기 때문에 실제로 회원가입한 유저의 유입 경로를 정확히 추적하기 어렵다. 또한 광고 차단 플러그인이나 브라우저 설정으로 인해 데이터가 누락될 수 있다.
따라서 GA는 그대로 유지하면서, 회원가입 시점에 유입 경로 데이터를 user 테이블에도 저장하는 방식으로 개선하기로 했다. 이렇게 하면 GA 데이터와 실제 회원가입 데이터를 교차 검증할 수 있고, 더욱 정확한 분석이 가능해진다.
UTM 파라미터와 Referrer 추적 개선
기존 시스템의 한계
기존 팀버핏 웹사이트는 UTM 파라미터의 모든 필드(utm_source, utm_medium, utm_campaign, utm_term, utm_content)가 존재해야만 세션에 UTM 값을 저장했습니다. 이는 다음과 같은 문제를 야기했습니다:
- 일부 마케팅 채널에서는 모든 UTM 필드를 사용하지 않아 추적 누락 발생
- 유입 경로 데이터가 부정확하여 마케팅 효과 측정이 어려움
개선된 UTM 추적 로직
마케팅팀의 요구사항을 반영하여 다음과 같이 로직을 개선했다. (아래 코드는 핵심 로직을 보여주기 위한 간략화된 예시다)
// 기존: 모든 UTM 필드가 있어야 저장
if (utmSource && utmMedium && utmCampaign && utmTerm && utmContent) {
sessionStorage.setItem('utm', JSON.stringify({ ... }));
}
// 개선: UTM 필드가 하나라도 있거나 referrer가 있으면 저장
if (utmSource || utmMedium || utmCampaign || utmTerm || utmContent || document.referrer) {
const trackingData = {
utm_source: utmSource,
utm_medium: utmMedium,
utm_campaign: utmCampaign,
utm_term: utmTerm,
utm_content: utmContent,
referrer: document.referrer
};
sessionStorage.setItem('tracking', JSON.stringify(trackingData));
}회원가입 시 세션 스토리지에 저장된 UTM 및 Referrer 데이터를 user 테이블에 저장하여 정확한 유입 경로 분석이 가능하도록 했습니다.
이번 프로젝트에서 발생한 버그와 문제 해결 방법
문제 1: 모바일에서 뒤쪽 Body 스크롤이 함께 작동하는 문제
문제 상황
모바일 환경에서 TrialSidebar가 전체 화면을 차지할 때, 사이드바 내부를 스크롤하려고 하면 뒤쪽 body의 스크롤도 함께 작동하여 사이드바 내부 스크롤을 제어하기 매우 어려웠습니다.
// 기존 스타일 - 문제가 있는 코드
export const Sidebar = styled.div<SidebarProps>`
position: fixed;
top: 0;
right: ${props => (props.$isOpen ? "0" : "-500px")};
width: 500px;
height: 100vh;
overflow-y: auto;
/* 이것만으로는 부족했습니다 */
overscroll-behavior: contain;
@media (max-width: 768px) {
width: 100%;
}
`;원인 분석
CSS의 overscroll-behavior: contain만으로는 body 스크롤을 완전히 막을 수 없었습니다. 특히 모바일 브라우저에서는 position: fixed 요소 내부를 스크롤할 때도 body 스크롤이 함께 발생하는 것이 일반적입니다.
해결 방법
useBodyScrollLock 커스텀 훅 개발
position: fixed + overflow: hidden 조합으로 body 스크롤을 완전히 차단하고, 스크롤 위치를 저장/복원하는 훅을 만들었습니다.
// useBodyScrollLock.ts
export const useBodyScrollLock = (isLocked: boolean) => {
useEffect(() => {
if (isLocked) {
// 현재 스크롤 위치 저장
const scrollY = window.scrollY;
// body를 fixed로 고정하여 스크롤 방지
document.body.style.position = "fixed";
document.body.style.top = `-${scrollY}px`;
document.body.style.width = "100%";
document.body.style.overflow = "hidden";
} else {
// 저장된 스크롤 위치 가져오기
const scrollY = document.body.style.top;
// body 스타일 복원
document.body.style.position = "";
document.body.style.top = "";
document.body.style.width = "";
document.body.style.overflow = "";
// 스크롤 위치 복원
if (scrollY) {
window.scrollTo(0, parseInt(scrollY || "0", 10) * -1);
}
}
// cleanup: 컴포넌트가 unmount 될 때 스타일 복원
return () => {
document.body.style.position = "";
document.body.style.top = "";
document.body.style.width = "";
document.body.style.overflow = "";
};
}, [isLocked]);
};적용
const TrialSidebar = ({ isOpen, onClose }: TrialSidebarProps) => {
// 사이드바가 열렸을 때 body 스크롤 잠금
useBodyScrollLock(isOpen);
// ... 나머지 코드
};결과
- ✅ iOS Safari를 포함한 모든 모바일 디바이스에서 body 스크롤 완전 차단
- ✅ 사이드바 닫을 때 원래 스크롤 위치로 자동 복원
- ✅ Layout Shift 없이 자연스러운 UX
문제 2: 모바일 키보드가 Input을 가리는 문제
문제 상황
모바일에서 로그인 화면의 Input 필드를 클릭하면 키보드가 올라오면서 Input이 키보드에 완전히 가려져 보이지 않는 문제가 발생했습니다.
- 테스트 환경: iPhone 11 Pro / Safari
- 증상: 휴대전화번호와 비밀번호 Input이 키보드에 완전히 가려짐
브라우저별 발생 여부
| 브라우저/기기 | 발생 여부 | 심각도 |
|---|---|---|
| iOS Safari | ⚠️ 발생 | 매우 심각 |
| iOS Chrome/Firefox | ⚠️ 발생 | 매우 심각 |
| Android Chrome | 🟡 부분 발생 | 중간 |
| Desktop | ❌ 미발생 | - |
원인 분석
iOS Safari의 특수성
- 키보드가 올라와도
window.innerHeight가 줄어들지 않음 position: fixed요소는 키보드를 "장애물"로 인식하지 못함
TrialSidebar의 구조적 문제
- 부모 컨테이너가
position: fixed - Input이 키보드 영역에 위치해도 자동으로 스크롤되지 않음
해결 방법
방법 2와 방법 3을 조합한 하이브리드 접근법을 사용했습니다:
- 방법 2: Input focus 시 자동 스크롤
- 방법 3: 모바일에서 큰 padding-bottom 추가
1) useInputScrollIntoView 커스텀 훅 개발
모바일에서만 작동하며, Input에 포커스가 갈 때 자동으로 스크롤하는 훅을 만들었습니다.
// useInputScrollIntoView.ts
export const useInputScrollIntoView = <T extends HTMLElement>() => {
const inputRef = useRef<T>(null);
useEffect(() => {
const element = inputRef.current;
if (!element) return;
const handleFocus = () => {
// 모바일 기기인지 확인 (768px 이하)
if (!isMobileDevice()) return;
// 키보드 애니메이션 완료 대기 (300ms)
setTimeout(() => {
// 부모 스크롤 컨테이너 찾기
const scrollParent = findScrollParent(element);
if (scrollParent) {
const elementRect = element.getBoundingClientRect();
const parentRect = scrollParent.getBoundingClientRect();
// 요소를 컨테이너의 중앙에 위치시키기 위한 스크롤 위치 계산
const scrollTop =
scrollParent.scrollTop +
elementRect.top -
parentRect.top -
parentRect.height / 2 +
elementRect.height / 2;
// 부드러운 스크롤
scrollParent.scrollTo({
top: scrollTop,
behavior: "smooth",
});
}
}, 300);
};
element.addEventListener("focus", handleFocus);
return () => {
element.removeEventListener("focus", handleFocus);
};
}, []);
return inputRef;
};
// 모바일 기기 감지
const isMobileDevice = (): boolean => {
if (typeof window === "undefined") return false;
return window.innerWidth <= 768;
};
// 스크롤 가능한 부모 요소 찾기
const findScrollParent = (element: HTMLElement): HTMLElement | null => {
let parent = element.parentElement;
while (parent) {
const { overflow, overflowY } = window.getComputedStyle(parent);
if (
overflow === "auto" ||
overflow === "scroll" ||
overflowY === "auto" ||
overflowY === "scroll"
) {
return parent;
}
parent = parent.parentElement;
}
return null;
};2) Input 컴포넌트에 forwardRef 지원 추가
기존 Input 컴포넌트가 ref를 지원하지 않아서 forwardRef를 추가했습니다.
// Before
const Input = ({ value, id, onChange, ...props }: InputProps) => {
return <CustomInput id={id} value={value} onChange={onChange} {...props} />;
};
// After
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ value, id, onChange, ...props }, ref) => {
return <CustomInput ref={ref} id={id} value={value} onChange={onChange} {...props} />;
},
);
Input.displayName = "Input";3) 모바일에서 충분한 padding-bottom 추가
// TrialLoginComponent.styles.ts
export const LoginContent = styled.div`
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
/* 모바일에서 키보드가 올라올 때를 대비한 여유 공간 확보 */
@media (max-width: 768px) {
padding-bottom: 350px;
}
`;4) TrialLoginComponent에 적용
const TrialLoginComponent = ({ onLoginSuccess }: TrialLoginComponentProps) => {
// 자동 스크롤을 위한 ref
const phoneNumberRef = useInputScrollIntoView<HTMLInputElement>();
const passwordRef = useInputScrollIntoView<HTMLInputElement>();
return (
<S.LoginContent>
<S.InputSection>
<Input
ref={phoneNumberRef}
label="휴대전화번호 ('-'제외)"
// ... props
/>
<Input
ref={passwordRef}
label="비밀번호"
type="password"
// ... props
/>
</S.InputSection>
</S.LoginContent>
);
};결과
모바일 (≤ 768px)
- ✅ Input 포커스 시 자동으로 화면 중앙으로 스크롤
- ✅ 키보드에 가려지지 않음
- ✅ 300ms 지연으로 키보드 애니메이션 완료 후 스크롤
데스크톱 (> 768px)
- ✅ 자동 스크롤 비활성화
- ✅ 불필요한 스크롤 없이 자연스러운 UX
모바일 이슈 해결 핵심 포인트
이번 모바일 이슈들을 해결하며 다음과 같은 교훈을 얻었다:
1. CSS만으로는 부족하다
overscroll-behavior: contain만으로는 body 스크롤을 막을 수 없었다- JavaScript를 활용한
position: fixed+ 스크롤 위치 저장/복원이 핵심 width: 100%로 Layout Shift 방지
2. 모바일 환경은 특별하다
- 모바일과 데스크톱을 분리하여 처리 (768px 기준)
- iOS Safari의 독특한 키보드 동작 (키보드 애니메이션 300ms 대기 필요)
- 자동 스크롤 + padding 조합으로 안정적인 UX 구현
3. 재사용성을 고려하라
- 커스텀 훅으로 분리하여 다른 모달/사이드바에서도 재사용 가능
- 명확한 문서화로 팀원들이 쉽게 이해하고 사용 가능
마치며
1. 마케팅과 개발의 협업
마케팅팀의 니즈를 정확히 이해하고 기술적으로 구현하는 과정에서 GA, GTM, UTM에 대한 깊은 이해를 얻었습니다. 단순히 코드를 작성하는 것이 아니라, 비즈니스 목표를 달성하기 위한 데이터 수집 시스템을 설계하는 경험이었습니다.
2. 사용자 경험의 중요성
기술적으로 완벽한 코드보다 사용자가 실제로 편하게 느끼는 UX가 더 중요하다는 것을 깨달았습니다. 특히 모바일 환경에서의 세심한 배려(스크롤 잠금, 키보드 대응 등)가 전환율에 직접적인 영향을 미칩니다.
3. 문제 해결 접근법
- CSS만으로 해결하려 하지 말 것 - JavaScript를 적절히 활용
- 모바일/데스크톱 분리 - 각 환경에 맞는 최적의 UX 제공
- 문서화의 중요성 - 왜 이 코드가 필요한지 명확히 기록
- 실제 기기 테스트 - 브라우저 개발자 도구만으로는 부족
4. 운동하고 싶다면 팀버핏으로