pile·
프론트엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

항공 프론트엔드 구축기 (4/10): “앱이냐”와 “좁냐”는 다른 질문이다

여기어때 항공 프론트엔드 팀이 웹·앱·데스크톱 세 환경 대응 중 "환경 구분"을 하나의 boolean으로 처리하려다 세 가지 독립적인 질문임을 발견하고 각각 별도 훅/CSS 변형자로 분리한 설계를 다룬다. 드롭다운을 바텀시트로 전환할 때 컴포넌트를 언마운트하지 않고 하나로 유지해 상태를 보존하는 방식도 함께 설명한다.

핵심 포인트
  • "앱 안에서 뜬 화면인가(isApp)", "화면이 좁은가(isMobile)", "마우스가 있는가(hoverable)"는 서로 독립적인 질문으로, 하나의 분기로 처리하면 어긋나는 경우가 생긴다.
  • isApp은 서버에서 주입되어 불변이고, isMobile은 리사이즈마다 갱신되며, 마우스 여부는 CSS @media 쿼리로만 처리한다.
  • SSR 환경에서 화면 너비를 모르는 문제는 useSyncExternalStore(서버용 기본값 지정 가능)로 해결했다.
  • 드롭다운과 바텀시트를 하나의 컴포넌트로 유지해 브레이크포인트를 넘어도 언마운트 없이 isOpen 상태를 보존했다.
  • 터치 디바이스에서 hover 색이 잔류하는 문제는 커스텀 Tailwind 변형자 hoverable:로 마우스 환경에서만 적용했다.
상세 정리
  • 발단: 데스크톱에서 드롭다운을 열어둔 채 브라우저 창을 좁히면 드롭다운이 닫히고 바텀시트가 열린 상태로 올라와야 한다는 요구사항에서 출발했다.
  • 질문 분리: isApp(usePlatform → 서버 주입, 불변), isMobile(useScreen → resize마다 갱신), 마우스 여부(CSS @media (hover: hover) and (pointer: fine)) 세 가지로 분리했다.
  • isApp 판단 3가지 조건: 웹으로 들어온 경우, 루트 레이아웃에서 웹이라고 넘겨준 경우, 개발자가 강제 설정(배포된 앱 빌드를 브라우저로 확인할 때)으로 각각 웹으로 판정한다.
  • hover 처리: 터치 디바이스에서 hover: 스타일을 그냥 쓰면 탭 후 hover 색이 잔류한다. addVariant('hoverable', '@media (hover: hover) and (pointer: fine)')로 커스텀 변형자를 추가하고 hoverable:hover:yf-bg-... 형태로 마우스 환경에서만 적용했다.
  • 브레이크포인트: 데스크톱 912px 이상, 모바일 미만. CSS에 "미만"이 없어 max: '911.98px'을 사용한다.
  • SSR/CLS: 서버는 window가 없어 화면 너비를 모른다. useSyncExternalStore를 사용해 서버용 초기값을 별도 지정했다.
  • CLS 해결 전략: "CSS로 가리기"(두 버전 모두 DOM에 렌더, 미디어쿼리로 하나 숨김)와 "자리 잡아두기"(서버에서 빈 박스, 브라우저에서 진짜 컴포넌트 교체) 두 방식 중 컴포넌트마다 선택했다.
  • 드롭다운 단일 컴포넌트 유지: 드롭다운이 isMobile을 직접 구독하지 않고 prop으로 받게 해서, 브레이크포인트를 넘어도 컴포넌트가 언마운트되지 않아 isOpen 상태가 유지된다.
  • 주의점: 컴포넌트가 죽지 않으므로 처음 찾아둔 스크롤 요소 같은 참조가 레이아웃 전환 후 잘못된 요소를 가리킬 수 있다.
왜 읽나웹·앱·데스크톱을 단일 코드베이스로 운영하면서 환경 구분 로직이 비대해지는 문제를 겪는 팀에게 세 가지 독립 질문 분리 패턴과 SSR/CLS 대응 전략을 구체적으로 제공한다.
여기어때 (GC컴퍼니)
여기어때 (GC컴퍼니) 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 프론트엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    항공 프론트엔드 구축기 (7/10): 창구를 하나만 두었습니다

    여기어때 항공 서비스 프론트엔드가 웹과 앱 웹뷰 두 환경에서 동일한 함수 호출로 동작하는 앱 브릿지 추상화 레이어를 설계한 과정을 다룬다. iOS·안드로이드 규약 차이와 "웹에 존재하지 않는 브릿지를 어떻게 호출하나"라는 문제를 단일 추상화 층으로 해결한 구현 사례다.

    요약 이어보기
    #react#typescript#webview+2