본문 바로가기
개인 프로젝트/개인 기록장

트러블 슈팅 - 중첩 Link 태그 개선

by 학습하는 청년 2025. 9. 7.

중첩 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>

 

  1. HTML 표준 준수
    <a> 안에 <button>은 유효한 HTML 구조이므로, HTML validator 및 접근성 도구에서 경고가 발생하지 않는다. 그렇기에 모든 브라우저에서 일관되게 동작한다.
  2. SEO 최적화 유지
    검색엔진이 상세 페이지 링크를 인식한다. 덕분에 Next.js가 자동으로 페이지를 미리 로드하여, 링크 복사, 새 탭 열기 등 브라우저 기본 기능이 가능하다.
  3. 접근성(Accessibility) 확보
    스크린리더가 '링크' + '버튼'으로 각각을 올바르게 인식한다. 이는 의미론적으로 각 요소의 목적이 명확함을 뜻한다. 또한, Tab 키로 카드와 버튼을 개별적으로 접근할 수있다.
  4. 이벤트 버블링 방지
    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. 터치 최적화
: 모바일에서 적절한 터치 타겟 크기 제공

댓글