중첩 Link 오류
사진과 같이 카드 컴포넌트 안에, 작성자가 내용을 수정 및 삭제할 수 있는 기능을 구현하려고 코드를 작성했다. 그러나 Link 태그 안에 Link 태그를 작성하니 에러가 발생했다.


<Link
href={`/reflections/${reflection.id}`}
className="block bg-white rounded-lg p-2 shadow-sm border hover:shadow-md transition-shadow cursor-pointer"
>
<div className="flex flex-col mb-3">
<div className="flex justify-between">
{ ... }
{showActions && reflection.is_own && (
<div className="flex items-center gap-1">
<Link
href={`/reflections/${reflection.id}/edit`}
onClick={handleEdit}
className="text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded-lg transition-colors"
title="편집"
>
<Edit className="w-4 h-4" />
</Link>
<button
onClick={handleDelete}
className="text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
title="삭제"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
)}
</div>
{ ... }
</Link>
이처럼 Link 안에 Link 태그를 사용하는 것은 HTML 표준상 불가능하다. 그래서 다음과 같은 구조로 해결했다.
const handleEdit = (e: React.MouseEvent) => {
e.stopPropagation();
onEdit(reflection.id);
};
{ ... }
<Link href="/detail"> // 외부: Link (a 태그)
<button onClick={handleEdit}> // 내부: button 태그
편집 아이콘
</button>
</Link>
- HTML 표준 준수
<a> 안에 <button>은 유효한 HTML 구조이므로, HTML validator 및 접근성 도구에서 경고가 발생하지 않는다. 그렇기에 모든 브라우저에서 일관되게 동작한다. - SEO 최적화 유지
검색엔진이 상세 페이지 링크를 인식한다. 덕분에 Next.js가 자동으로 페이지를 미리 로드하여, 링크 복사, 새 탭 열기 등 브라우저 기본 기능이 가능하다. - 접근성(Accessibility) 확보
스크린리더가 '링크' + '버튼'으로 각각을 올바르게 인식한다. 이는 의미론적으로 각 요소의 목적이 명확함을 뜻한다. 또한, Tab 키로 카드와 버튼을 개별적으로 접근할 수있다. - 이벤트 버블링 방지
e.stopPropagetion() 함수를 사용하여, 편집 버튼을 클릭하였을 때 Link 태그로 작성된 카드 자체가 클릭되지 않도록 하였다. 즉, 내가 원하는 기능(편집)만 동작하도록 이벤트 버블링을 적용하였다.
cf. button 태그에는 기본 동작이 존재하지 않기에, e.preventDefault()는 사용하지 않았다.
기본 동작?
1) 폼 안의 submit 버튼
2) a 태그 스타일 버튼
cf) Link 태그는 클릭 시 기본 브라우저 네비게이션을 자동으로 차단해준다. 로직이 이미 내장되어 있다.
// 케이스 1: 폼 안의 submit 버튼
<form onSubmit={handleSubmit}>
<button type="submit" onClick={(e) => {
e.preventDefault(); // 폼 제출 방지
customLogic();
}}>제출</button>
</form>
// 케이스 2: a 태그 스타일 버튼
<a href="#" onClick={(e) => {
e.preventDefault(); // # 이동 방지
handleClick();
}}>클릭</a>
======
// Next.js Link (preventDefault 불필요)
<Link href="/detail" onClick={(e) => {
// e.preventDefault(); // 불필요!
customLogic();
}}>링크</Link>
하지만 실제로 해보니 달랐다!
편집하는 페이지로 이동하는 것이 아니라 부모 요소의 카드 상세 페이지로 이동하는 문제가 발생했다. 결국 e.prevetDefault()를 추가함으로써 해결할 수 있었다.
- button 요소가 Link 컴포넌트 내부에 있을 때, Link의 클릭 이벤트가 여전히 실행될 수 있다.
- e.stopPropagation() 만으로는 이미 시작된 Link의 네비게이션을 완전히 막지 못할 수 있기에, e.preventDefault()가 추가로 필요하다
Q. 지금처럼 Link + button이 아니라, 모두를 button으로 사용해도 해결되지 않는가?
<div
className="card cursor-pointer"
onClick={() => router.push('/detail')}
>
<div>카드 내용</div>
<button onClick={(e) => {
e.stopPropagation();
handleEdit();
}}>편집</button>
</div>
해결은 된다. 오류 메시지 역시 나타나지 않는다.
장점
- 이벤트 제어 단순함: 모든 클릭을 JS로 처리
- 복잡한 로직 가능: 조건부 네비게이션, 권한 체크 등 자유로움
- 중첩 문제 없음: HTML 표준 위반 걱정 없음
- 메모리 효율성: Link 컴포넌트보다 가벼움
단점
- SEO 약점: 검색엔진이 내부 페이지 링크 인식 불가
- 사용자 경험 제약: 우클릭 메뉴, 새 탭 열기, 링크 복사 불가능
- 접근성 문제: 스크린리더가 네비게이션 요소로 인식 못함
- 프리페칭 없음: 페이지 로딩 최적화 불가
- 브라우저 기능 상실: 주소 미리보기, Ctrl + 클릭 등 불가능
Link 태그와 button 태그 설명
| Link | button | |
| 기술 | 1. 자동 프리페칭: Next.js가 화면에 보이는 Link를 미리 로드 2. 클라이언트 사이드 라우팅: 페이지 새로고침 없이 빠른 네비게이션 3. 브라우저 히스토리: 뒤로가기/앞으로가기 정상 작동 4. 상태 유지: 스크롤 위치, 폼 데이터 등 SPA 상태 보존 |
1. 명확한 의도: 액션 수행을 위한 요소임을 명시 2. 이벤트 독립성: Link의 네비게이션과 독립적인 이벤트 처리 3. 상태 관리: disabled, loading 등 상태 표현 가능 4. 폼 연동: type="submit" 등으로 폼과 자연스러운 연동 |
| 사용자 경험 | 1. 우클릭 메뉴: "새 탭에서 열기", "링크 복사" 등 제공 2. Ctrl+클릭: 새 탭에서 열기 동작 3. 마우스 호버: 브라우저 하단에 URL 미리보기 4. 접근성: 스크린리더가 "링크"로 인식하여 사용자에게 알림 |
1. 시각적 피드백: 호버, 클릭 시 버튼다운 시각적 반응 2. 키보드 접근: Tab으로 포커스, Enter/Space로 활성화 3. 접근성: 스크린리더가 "버튼"으로 인식하여 액션 요소임을 알림 4. 터치 최적화: 모바일에서 적절한 터치 타겟 크기 제공 |
'개인 프로젝트 > 개인 기록장' 카테고리의 다른 글
| 트러블 슈팅 - 로그아웃 세션 유지 문제 (0) | 2025.09.11 |
|---|---|
| 회원가입 - Supabase 이메일 인증 vs JWT (0) | 2025.09.10 |
| CI/CD와 테스트 (0) | 2025.09.07 |
| UX 개선 - 소프트 삭제, 낙관적 업데이트(삭제와 복구) (0) | 2025.09.04 |
| 트러블 슈팅 - 제약조건 변경에 따른 프로필 생성 누락 (0) | 2025.09.04 |
댓글