{isPlaying && vid ? (
{item.title}
{formatDate(item.publishedAt)}

목표 요약
- 모바일: 캐러셀(드래그) 탭 + 상단 여백 축소 + Compact 프로필 + 날씨 비표시
- 데스크탑: 탭 중앙정렬 유지 + Full 프로필 + 날씨 표시 + 방송국 3×3 그리드 고정
- 성능: iframe 즉시로딩 제거(썸네일 → 클릭 시 임베드), 불필요 렌더/네트워크 최소화, LCP/TTI 개선
- {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>
<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>
<div className="hidden md:flex justify-center gap-6" role="tablist">
{/* underline + active 강조 */}
</div>
- <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 (
{formatDate(item.publishedAt)}
); })}
<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>
이번 작업은 뷰포트별 정보량 제어 + 조건부 렌더 + 지연 임베드라는 세 축으로 구성했다. 결과적으로 모바일 가독성·터치성 향상, 데스크탑 일관성 유지, 초기 로딩 비용 절감이라는 세 마리 토끼를 잡았다. 이후에는 preconnect/IntersectionObserver/이미지 최적화까지 단계적으로 적용해 추가 이득을 확보할 수 있다.
한 줄 요약: “필요한 순간에만, 필요한 만큼만 렌더/다운로드.” 이것이 오늘의 와이즈한 리빌드였다.
| MJS 광고 캐러셀 & 실시간 검색 순위 집계 Component 설계 가이드 (6) | 2025.08.18 |
|---|---|
| MJS 명대뉴스 반응형 구현 (3) | 2025.08.16 |
| MJS _ 명대방송국 연계 (1) | 2025.08.08 |
| 백엔드에서 요구한 데이터 구조에 맞춘 API 타입 설계 (3) | 2025.07.06 |
| <React + Vue.js> 프로젝트 내 회원가입 기능 구현 가이드 (0) | 2025.01.12 |