

대상 코드: NewsCard 컴포넌트 (article, aria-labelledby, aspect-ratio, onError 캐스팅, React.memo)
GPT의 흐름을 파악하기 위한 “왜 이렇게 썼는지”를 끝까지 파고드는 설명형 레퍼런스입니다.
시맨틱 태그부터 접근성, 이미지 비율, 이벤트 타입 캐스팅, 메모이제이션까지 한 번에 훑습니다.
1) <article>는 무엇이고, 언제 쓰나?
시맨틱 태그(의미 있는 HTML 태그) 중 하나로, 블로그 글/뉴스 카드/피드 아이템처럼 독립적으로 의미가 성립하는 콘텐츠 묶음을 감싸는 데 씁니다.
- 특징
- 문서 내에서 단독 재사용 가능한 콘텐츠 블록.
- RSS, 북마크, 공유 등 콘텐츠 단위로 식별되길 원하는 영역.
- 내부에 제목(<h1~h6>)이 있는 것이 일반적.
- 언제 <div> 대신 <article>?
- 이 카드 하나가 하나의 글/뉴스로 성립한다면 <article>이 더 적합.
- 목록의 각 아이템이 독립 문서처럼 의미가 있으면 article, 단순 레이아웃 컨테이너면 div.
우리의 뉴스 카드: 링크/제목/요약/날짜가 있고, 단독으로 전달될 수 있으니 <article> 채택이 타당.
2) ARIA와 aria-labelledby 한 번에 이해하기
**ARIA(Accessible Rich Internet Applications)**는 스크린리더 같은 보조기술에 콘텐츠 의미를 확실히 알려주는 접근성 표준 속성 세트입니다.
- aria-labelledby: 이 요소의 **이름(레이블)**이 어디에 쓰였는지를 id 참조로 가리킵니다.
- 스크린리더는 <article>의 이름을 h3의 텍스트로 인식.
- 의미: "이 카드의 대표 텍스트는 저 제목이다"를 명시.
- <article aria-labelledby={`news-${id}-title`}> <h3 id={`news-${id}-title`}>제목</h3> </article>
- 왜 필요한가?
- 시각적으로는 제목이 눈에 보이지만, 보조기술은 구조를 따로 이해해야 함.
- ID 연결로 요소 간 명확한 관계를 제공 → 접근성 점수/실사용성 향상.
- 추가 팁
- 날짜는 <time dateTime="ISO8601">표시용</time>으로 작성하면 기계가독성↑.
- 이미지는 alt를 정확히: 의미 있는 썸네일이면 간결 설명, 장식이면 빈 문자열 alt="".
3) aspect-[16/9]는 무엇을 보장하나?
Tailwind의 aspect-[16/9]는 CSS aspect-ratio: 16 / 9;를 적용합니다. 가로:세로 비율 유지가 핵심.
- 왜 필요한가?
- 썸네일 원본 비율이 제각각이면 CLS(레이아웃 점프) 발생.
- 래퍼에 고정 비율을 주고, 이미지를 object-cover로 채우면 안정적 그리드 유지.
- 카드 구조
- 래퍼: 비율 상자.
- 이미지: 비율 상자 안에서 잘려도 좋으니 꽉 채우기.
- <div className='aspect-[16/9] w-full overflow-hidden bg-gray-100'> <img className='h-full w-full object-cover' ... /> </div>
- 대안
- 고정 높이/너비 대신 aspect-ratio가 더 유연하고 반응형에 적합.
4) onError에서 (e.currentTarget as HTMLImageElement)로 캐스팅한 이유
React 이벤트 객체는 제네릭 타입으로, 핸들러 시그니처에 정확한 대상 타입을 지정하지 않으면 currentTarget의 타입이 느슨해집니다.
- 현재 코드
- currentTarget은 이벤트가 바인딩된 **그 요소(여기선 <img>)**를 가리킴.
- 타입 시스템에 “이건 이미지야”를 알려줘야 src 접근을 허용.
- onError={(e) => { (e.currentTarget as HTMLImageElement).src = fallback; }}
- 더 타입 안전하게 쓰기
- 핸들러 시그니처에 **명시적으로 HTMLImageElement**를 지정하면 캐스팅 불필요.
- function handleImgError(e: React.SyntheticEvent<HTMLImageElement, Event>) { e.currentTarget.src = fallback; } <img onError={handleImgError} ... />
- target vs currentTarget
- target: 실제 이벤트가 발생한 가장 안쪽 요소.
- currentTarget: 핸들러가 바인딩된 요소. 여기선 <img>.
- 일반적으로 React에선 currentTarget을 신뢰.
5) React.memo로 내보낸 이유와 주의점
React.memo는 props가 바뀌지 않으면 리렌더를 건너뛰게 하는 고차 컴포넌트입니다.
export default memo(NewsCard);
- 왜 유효한가?
- 뉴스 리스트는 카드가 많이 반복됨.
- 부모가 리렌더돼도 각 카드 props가 동일하면 재렌더 생략 → 성능 이점.
- 주의 포인트
- 참조 타입 props(객체/배열/함수)가 매번 새 인스턴스면 효과 감소.
- 필요 시 memo(Comp, areEqual)로 사용자 비교 함수 제공 가능.
- 간단한 비교 함수 예시
- 비교 범위를 최소화해 유지보수 난도를 낮추는 것도 현실적 선택.
- export default memo(NewsCard, (prev, next) => { return ( prev.page === next.page && prev.index === next.index && prev.news.link === next.news.link && prev.news.title === next.news.title && prev.news.imageUrl === next.news.imageUrl && prev.news.summary === next.news.summary && prev.news.category === next.news.category && prev.news.date === next.news.date ); });
- 언제 굳이 안 쓰나?
- 카드 수가 적거나, 상태 구조 상 이미 리렌더 비용이 작을 때.
- 불필요한 최적화는 복잡도만 추가.
6) default Parameter의 정적 자산 위치
- fallbackSrc = '/default-thumbnail.png'처럼 루트 경로를 쓰면, 파일은 public/default-thumbnail.png에 있어야 함.
- 번들링에 포함시키려면 import fallbackImg from '.../assets/...png' 후 src={news.imageUrl || fallbackImg} 사용.
- onError에서도 같은 값을 대입해 무한 루프 방지 (이미 fallback이면 재설정하지 않기).
const fallback = fallbackSrc ?? '/default-thumbnail.png';
<img
src={news.imageUrl?.trim() || fallback}
onError={(e) => {
if (e.currentTarget.src !== window.location.origin + fallback) {
e.currentTarget.src = fallback;
}
}}
...
/>
7) 이미지 성능 디테일: loading, decoding, sizes
- loading='lazy': 뷰포트 근처에서 로딩 시작. 초기 LCP 이미지는 eager 고려.
- decoding='async': 디코딩 비동기 힌트(상황에 따라 체감 미미).
- sizes: 반응형 레이아웃 입히기! 그리드 열 수와 실제 카드 폭에 맞춰 선언.
8) 타이포/라인 클램프/호버 효과의 의도
- 라인 클램프: line-clamp-2, md:line-clamp-3로 화면이 넓어지면 더 많은 텍스트를 노출.
- 호버 스케일: group-hover:scale-[1.02]로 카드 전체 호버 시 이미지만 미세 확대 → 생동감.
- 타이포 단계: text-base → md:text-lg로 가독성/밀도 균형 조절.
interface NewsCardProps {
news: NewsInfo;
index: number;
page: number;
fallbackSrc?: string;
}
const ITEMS_PER_PAGE = 8;
function NewsCard({ news, index, page, fallbackSrc }: NewsCardProps) {
const id = (page - 1) * ITEMS_PER_PAGE + index + 1;
const fallback = fallbackSrc ?? '/default-thumbnail.png';
function handleImgError(e: React.SyntheticEvent<HTMLImageElement, Event>) {
// 이미 fallback이면 재설정 X → 무한루프 방지
const fallbackUrl = new URL(fallback, window.location.origin).toString();
if (e.currentTarget.src !== fallbackUrl) {
e.currentTarget.src = fallbackUrl;
}
}
return (
<article className='group relative flex flex-col overflow-hidden rounded-2xl border-grey-20 bg-white shadow-sm transition hover:shadow-lg' aria-labelledby={`news-${id}-title`}>
<a href={news.link} target='_blank' rel='noopener noreferrer' className='block'>
<div className='aspect-[16/9] w-full overflow-hidden bg-gray-100'>
<img
src={news.imageUrl?.trim() || fallback}
onError={handleImgError}
alt={news.title}
className='h-full w-full object-cover transition duration-300 group-hover:scale-[1.02]'
loading='lazy'
sizes='(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:768px) 50vw, 100vw'
/>
</div>
<div className='flex flex-col gap-2 p-4 md:p-5'>
<div className='flex items-center gap-2 text-xs md:text-[13px]'>
<span className='inline-flex items-center rounded-full px-2 py-0.5 font-medium text-gray-600'>
{news.category}
</span>
<time className='text-gray-500' dateTime={new Date(news.date).toISOString()}>{news.date}</time>
</div>
<h3 id={`news-${id}-title`} className='line-clamp-2 text-base font-semibold leading-snug md:text-lg'>
{news.title}
</h3>
<p className='line-clamp-2 text-sm text-grey-40 md:line-clamp-3'>{news.summary}</p>
</div>
</a>
</article>
);
}
export default memo(NewsCard);
마무리
- <article>/ARIA는 의미와 접근성을, aspect-ratio는 레이아웃 안정성을, 이벤트 타입 명시는 타입 안전성을, memo는 렌더링 성능을 잡습니다.
'Project Records > Project : Team Nova MJ Search' 카테고리의 다른 글
| MJS : 학사일정 컴포넌트 및 자유게시판 HOT 게시물 컴포넌트 연동 문서 (0) | 2025.08.19 |
|---|---|
| MJS 광고 캐러셀 & 실시간 검색 순위 집계 Component 설계 가이드 (6) | 2025.08.18 |
| MJS 반응형 리빌드 & 방송국 페이지 성능 튜닝 (4) | 2025.08.14 |
| MJS _ 명대방송국 연계 (1) | 2025.08.08 |
| 백엔드에서 요구한 데이터 구조에 맞춘 API 타입 설계 (3) | 2025.07.06 |