Project Records/Project : Team Nova MJ Search

MJS 반응형 리빌드 & 방송국 페이지 성능 튜닝

Frisbeen 2025. 8. 14. 22:48

WHAT IS RESPONSIVE DESIGN?

목표 요약

  • 모바일: 캐러셀(드래그) 탭 + 상단 여백 축소 + Compact 프로필 + 날씨 비표시
  • 데스크탑: 탭 중앙정렬 유지 + Full 프로필 + 날씨 표시 + 방송국 3×3 그리드 고정
  • 성능: iframe 즉시로딩 제거(썸네일 → 클릭 시 임베드), 불필요 렌더/네트워크 최소화, LCP/TTI 개선

0. 변경 전 주요 Pain Points

  • 탭이 모바일에서 줄바꿈/과밀 → 터치 정확도·가독성 저하.
  • Navbar–Header–SearchBar 간격이 모바일에서 과도.
  • Profile/Weather가 모바일에서도 동일 무게로 렌더 → 초기 비용↑.
  • 방송국 페이지에서 iframe 다건 즉시 로딩 → 초기 렌더/네트워크 폭증, TTI 지연.
  • 데스크탑 초대형 해상도에서 4열까지 늘어나 일관된 3×3 레이아웃 유지 실패.

1. 레이아웃(LayoutForMain) — 뷰포트별 UI 전략

1.1 정책

  • Header: 모바일 숨김, md 이상에서만 노출 → 상단 여백 최소화.
  • Profile: 모바일은 Compact, 데스크탑은 Full.
  • Weather: 모바일 비노출(필요 시 부모에서 조건부 렌더로 네트워크까지 차단).
  • 우측 컬럼: 모바일 미렌더(hidden md:flex)로 DOM/연산 절감.

1.2 핵심 Diff

- {shouldShowHeader && <Header />}
+ {shouldShowHeader && <div className="hidden md:block"><Header /></div>}

- <div className='flex flex-col md:flex-row gap-4 mt-6'>
+ <div className='flex flex-col md:flex-row gap-3 md:gap-4 mt-2 md:mt-6'>

  <div className='min-w-0 w-full md:w-2/3 flex flex-col gap-3'>
+   <div className="md:hidden"><ProfileComponent /></div>  {/* 모바일 Compact */}
    <SearchBar />
    {children}
  </div>

- <div className='min-w-0 w-full md:w-1/3 flex flex-col gap-3'>
-   <div className='hidden'><ProfileComponent /></div>
-   <div className='hidden'><WeatherComponent /></div>
- </div>
+ <div className='hidden md:flex min-w-0 w-full md:w-1/3 flex-col gap-3'>
+   <ProfileComponent />  {/* 데스크톱 Full */}
+   <WeatherComponent />
+ </div>

1.3 이유

  • 모바일 퍼스트: 화면 작은 곳에 정보 최소화 → 초기 렌더/레이아웃 계산량 축소.
  • 조건부 렌더: 보여줄 때만 마운트 → 효과적인 네트워크·CPU 절약.

2. 탭(TabComponent) — 모바일 캐러셀(드래그) + 데스크탑 라인형

2.1 모바일 캐러셀 UX

  • 컨테이너: overflow-x-auto + scroll-smooth + snap-x snap-mandatory.
  • 아이템: snap-center shrink-0 + Pill 스타일(라운드, 보더, 그림자 약하게).
  • 클릭 시 활성 탭을 중앙으로: el.scrollIntoView({ behavior: 'smooth', inline: 'center' }).
<div
  role="tablist"
  className="no-scrollbar -mx-4 px-4 overflow-x-auto scroll-smooth snap-x snap-mandatory whitespace-nowrap flex gap-3 py-1 md:hidden"
>
  {tabs.map(([label, value]) => (
    <button
      key={value}
      ref={(el) => (itemRefs.current[value] = el)}
      onClick={() => { setCurrentTab(value); itemRefs.current[value]?.scrollIntoView({ behavior:'smooth', inline:'center', block:'nearest' }); }}
      className={`snap-center shrink-0 px-4 py-2 rounded-full text-sm border transition ${currentTab===value ? 'bg-blue-20 text-white border-blue-20 shadow-sm' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'}`}
    >{label}</button>
  ))}
</div>

2.2 데스크탑 라인형 유지

<div className="hidden md:flex justify-center gap-6" role="tablist">
  {/* underline + active 강조 */}
</div>

2.3 접근성

  • role="tablist", role="tab", aria-selected, aria-controls 부여.
  • 탭 포커스 이동(Tab/Shift+Tab) 테스트 필수.

3. ProfileComponent — Compact / Full 이중 레이아웃

3.1 구조

  • 모바일: md:hidden 블록에서 Compact(아바타+이름+My 버튼)
  • 데스크탑: hidden md:block 블록에서 Full(이메일/외부 링크/로그아웃)

3.2 라우팅 주의

  • 로그인/가입 링크는 절대 경로(/login, /register)를 사용해 경로 꼬임 방지.

4. 방송국 페이지(Broadcast) — 성능 최적화 초상세

4.1 목표

  • 데스크탑: 항상 3×3 그리드로 일관된 뷰.
  • 초기 렌더에서 iframe 0개(=썸네일만) → 카드 클릭 시 해당 카드만 iframe 로드.
  • 네트워크·페인트·레이아웃 비용 최소화.

4.2 핵심 코드

- <section className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 md:gap-6 mt-6'>
+ {/* 모바일 1열 → 태블릿 2열 → 데스크탑 3열(고정) */}
+ <section className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3 md:gap-5 lg:gap-6 mt-6'>
{(broadcast ?? []).slice(0, 9).map((item) => {
  const vid = extractYoutubeId(item.url);
  const isPlaying = playingId === vid;
  return (
{isPlaying && vid ? (

{item.title}

{formatDate(item.publishedAt)}

); })}

4.3 성능 이득(논리 근거)

  • 즉시 로드 iframe → 0개: 초기에 로드되는 리소스가 이미지로 대체되어 전송 바이트·연결 수 급감.
  • loading="lazy" 이미지/iframe: 뷰포트 외 리소스 지연 로드 → LCP/TTI 개선.
  • 데스크탑 3×3 고정: 초대형에서도 4열 확장 방지 → 행 수 증가/재배치 비용 방지.
  • 페이지 전환 시 setPlayingId(null): 잔류 재생 상태 제거 → 불필요한 멀티 미디어 스레드/CPU 점유 방지.

4.4 추가 최적화 옵션(선택 적용)

  1. preconnect (index.html)

  1. 이미지 규격 최적화: 썸네일 srcset/sizes 지정으로 고해상도 기기 효율 개선.
  2. IntersectionObserver: 카드가 뷰포트 진입 시 썸네일 프리로드 또는 임베드 전환.
  3. sandbox 속성: 정책상 필요 시 iframe 보안 범위 제한(예: sandbox="allow-scripts allow-same-origin allow-presentation").

5. 측정(벤치마킹) 가이드 — 바뀐 게 진짜 빠른지 증명하기

5.1 Lighthouse & DevTools

  • Lighthouse(Performance, Best Practices, SEO) 3회 평균.
  • Performance 패널: CPU 4× Slowdown, 네트워크 Fast 3G/Slow 4G로 재현.
  • 지표: LCP, FID/INP, CLS, TBT, TTI, Requests, Transfer Size.

5.2 간이 계측 스니펫

<script>
  performance.mark('app-start');
  window.addEventListener('load', () => {
    performance.mark('app-loaded');
    performance.measure('boot', 'app-start', 'app-loaded');
    const m = performance.getEntriesByName('boot')[0];
    console.log('[perf] boot ms:', Math.round(m.duration));
  });
</script>
  • 변경 전/후 로그 비교로 체감 외 수치 확인.

5.3 기대 변화(경험칙)

  • 초기 iframe 9→0: 네트워크 커넥션 수/전송량 수배 감소.
  • LCP 수백 ms ~ 수 초 단축(환경 의존). 이미지 캐싱 효과로 재탐색 시 더 빠름.

6. 접근성 & 사용성 체크리스트

  • 탭: role/aria-* 준수, 키보드 포커싱 동작 확인.
  • 버튼: aria-label(재생) 제공.
  • 제목/이미지: 의미 있는 alt 적용.
  • 포커스 링 시각화(디자인 정책에 맞추어 focus-visible 스타일 제공).

7. 실패/함정 회피 팁

  • 상대 경로 Link(to="login") → 절대 경로(/login)로 수정하지 않으면 라우팅 꼬임.
  • 모바일에서 우측 컬럼 DOM을 숨기기만 하고 렌더하면 훅/네트워크가 도는 경우 → hidden md:flex + 부모 조건부 렌더로 차단 고려.
  • xl:grid-cols-4 방치 시 4열 확장 → 3×3 유지 실패.
  • 썸네일 없는 경우 key/alt 빈 문자열 주의 → key=vid || item.url, alt=item.title로 방어.

8. To‑Verify (릴리즈 체크)

Pass ! 


9. 결론

이번 작업은 뷰포트별 정보량 제어 + 조건부 렌더 + 지연 임베드라는 세 축으로 구성했다. 결과적으로 모바일 가독성·터치성 향상, 데스크탑 일관성 유지, 초기 로딩 비용 절감이라는 세 마리 토끼를 잡았다. 이후에는 preconnect/IntersectionObserver/이미지 최적화까지 단계적으로 적용해 추가 이득을 확보할 수 있다.

한 줄 요약: “필요한 순간에만, 필요한 만큼만 렌더/다운로드.” 이것이 오늘의 와이즈한 리빌드였다.