현록

HTML button과 a 태그 차이

웹 화면에서 링크와 버튼은 비슷하게 보일 수 있다.
둘 다 클릭할 수 있고, CSS를 적용하면 같은 모양으로 만들 수도 있다.

하지만 HTML에서 <a><button>의 역할은 다르다.
<a>는 다른 URL이나 문서의 위치로 이동할 때 사용한다.
<button>은 현재 화면에서 어떤 동작을 실행할 때 사용한다.

가장 짧게 구분하면 목적지가 있으면 <a>, 실행할 동작이 있으면 <button>이다.

a 태그의 역할

<a>는 anchor 요소다.
href에 지정된 URL로 이동하는 하이퍼링크를 만든다.

<a href="/posts">글 목록으로 이동</a>

사용자가 링크를 선택하면 브라우저는 /posts로 이동한다.
이동 대상은 같은 사이트의 페이지일 수도 있고 외부 사이트일 수도 있다.

<a href="https://developer.mozilla.org/">MDN Web Docs</a>

같은 문서 안의 특정 위치로 이동할 수도 있다.

<a href="#references">참고 자료로 이동</a>

<h2 id="references">참고 자료</h2>

mailto:tel:처럼 브라우저가 이해하는 다른 URL scheme도 사용할 수 있다.

<a href="mailto:hello@example.com">이메일 보내기</a>
<a href="tel:+821012345678">전화 걸기</a>

핵심은 사용자가 링크를 따라갈 수 있는 실제 목적지가 있다는 점이다.

button 태그의 역할

<button>은 사용자가 활성화해 동작을 실행하는 요소다.
폼 제출, 메뉴 열기, 모달 닫기, 목록 삭제, 재생과 일시 정지 같은 행동에 사용한다.

<button type="button">메뉴 열기</button>

type="button"은 자체적인 기본 동작이 없는 일반 버튼을 만든다.
JavaScript 이벤트나 HTML의 선언적 기능을 연결해 원하는 동작을 실행할 수 있다.

<button type="button" id="menu-button">메뉴 열기</button>
const menuButton = document.querySelector('#menu-button')

menuButton?.addEventListener('click', () => {
  document.querySelector('#menu')?.toggleAttribute('hidden')
})

버튼은 주소를 나타내는 요소가 아니다.
현재 문서나 애플리케이션의 상태를 바꾸는 명령에 가깝다.

두 요소의 핵심 차이

두 요소는 화면 모양이 아니라 행동의 의미로 선택한다.

  • <a>는 다른 URL이나 문서 위치로 이동한다.
  • <button>은 현재 화면에서 동작을 실행한다.
  • <a>의 핵심 속성은 목적지를 담는 href다.
  • <button>의 핵심 속성은 행동 종류를 정하는 type이다.
  • 링크는 새 탭 열기, 주소 복사, 북마크 같은 브라우저 기능과 연결된다.
  • 버튼은 폼 제출과 disabled 상태 같은 form control 기능을 가진다.

글 상세 페이지로 이동한다면 <a>가 맞다.
현재 글을 좋아요 목록에 추가한다면 <button>이 맞다.

이동에 a 태그를 쓰는 이유

브라우저는 실제 링크에 여러 기본 기능을 제공한다.

사용자는 링크를 새 탭에서 열 수 있다.
링크 주소를 복사하거나 북마크할 수도 있다.
검색 엔진과 보조 기술도 href를 가진 링크를 이동 경로로 이해한다.

<a href="/products/keyboard">키보드 상세 보기</a>

이동을 버튼과 JavaScript로 구현하면 이런 기본 기능을 잃기 쉽다.

<button type="button" id="product-link">키보드 상세 보기</button>
document.querySelector('#product-link')?.addEventListener('click', () => {
  window.location.href = '/products/keyboard'
})

클릭하면 이동은 되지만 브라우저는 이 요소를 링크로 다루지 않는다.
마우스 오른쪽 메뉴에서 링크 주소를 복사하거나 새 탭으로 여는 일반적인 사용 방식도 기대하기 어렵다.

이동 목적지가 있다면 처음부터 <a href="...">를 사용하는 편이 단순하고 의미도 정확하다.

URL 경로와 검색 조건을 나누는 기준은 URL path parameter와 query string 차이에서 확인할 수 있다.

동작에 button 태그를 쓰는 이유

버튼은 사용자가 명령을 실행한다는 의미를 브라우저와 보조 기술에 전달한다.

<button type="button">장바구니에 담기</button>
<button type="button">알림 닫기</button>
<button type="button">비밀번호 표시</button>

세 예시 모두 다른 페이지로 이동하는 것이 아니라 현재 상태를 바꾼다.

버튼은 disabled 속성도 지원한다.

<button type="button" disabled>저장 중</button>

비활성화된 버튼은 사용자가 누를 수 없고 일반적인 키보드 초점 대상에서도 제외된다.
작업이 진행 중이거나 필수 입력이 끝나지 않은 상태처럼, 동작을 잠시 막아야 할 때 사용할 수 있다.

링크에는 같은 의미의 네이티브 disabled 속성이 없다.
<a disabled>를 작성해도 버튼처럼 자동으로 비활성화되지 않는다.

button type의 종류

<button>type은 버튼이 어떤 기본 행동을 하는지 정한다.

type="button"은 일반 동작용 버튼이다.

<button type="button">도움말 열기</button>

type="submit"은 연결된 form을 제출한다.

<form action="/search" method="get">
  <input type="search" name="q" />
  <button type="submit">검색</button>
</form>

type="reset"은 form control을 초기값으로 되돌린다.

<button type="reset">입력 초기화</button>

실무에서는 사용자가 작성한 내용을 실수로 지울 수 있어 reset 버튼을 신중하게 사용한다.

세 종류를 구분하면 의도하지 않은 폼 제출을 막을 수 있다.

form 내부의 기본 submit 동작

<form> 안에서 button의 type을 생략하면 submit 버튼으로 동작할 수 있다.

<form>
  <input name="nickname" />
  <button>중복 확인</button>
</form>

개발자는 중복 확인만 실행하려고 했지만 버튼을 누르는 순간 form 전체가 제출될 수 있다.
페이지가 새로고침되거나 입력 상태가 사라지는 문제로 이어지기도 한다.

폼을 제출하지 않는 동작이라면 type="button"을 명시한다.

<form>
  <input name="nickname" />
  <button type="button">중복 확인</button>
  <button type="submit">가입하기</button>
</form>

버튼의 목적이 코드에서 바로 보이고 브라우저의 기본 동작도 예측하기 쉬워진다.

가짜 버튼으로 만든 a 태그

href="#"를 넣은 링크에 click 이벤트를 연결해 버튼처럼 사용하는 패턴이 있다.

<a href="#" id="delete-button">삭제</a>
document.querySelector('#delete-button')?.addEventListener('click', (event) => {
  event.preventDefault()
  deleteItem()
})

겉으로는 동작하지만 링크의 의미와 실제 행동이 맞지 않는다.
JavaScript가 실행되지 않으면 페이지 위쪽으로 이동할 수 있고, 새 탭 열기나 주소 복사도 쓸모없는 결과를 만든다.
스크린 리더는 이 요소를 버튼이 아니라 링크로 안내한다.

URL 이동 없이 삭제만 실행한다면 <button>이 맞다.

<button type="button" id="delete-button">삭제</button>

href="javascript:void(0)"도 같은 문제를 가진다.
실제 목적지가 없는 링크를 만들기보다 행동에 맞는 HTML 요소를 선택해야 한다.

가짜 링크로 만든 button 태그

버튼을 눌렀을 때 location.href를 바꿔 페이지를 이동하는 패턴도 피하는 편이 좋다.

<button type="button" id="detail-button">상세 보기</button>
document.querySelector('#detail-button')?.addEventListener('click', () => {
  location.href = '/products/42'
})

실제 목적지가 /products/42로 존재하므로 링크가 더 알맞다.

<a href="/products/42">상세 보기</a>

React Router, Next.js 같은 프레임워크를 사용해도 기준은 같다.
클라이언트 라우팅용 Link 컴포넌트는 최종적으로 링크 의미와 브라우저 동작을 유지해야 한다.
현재 상태를 바꾸는 이벤트는 button으로 구현한다.

키보드 동작의 차이

네이티브 링크와 버튼은 키보드에서도 각자 기대되는 방식으로 동작한다.

href가 있는 링크는 보통 Enter로 활성화한다.
버튼은 EnterSpace로 활성화할 수 있다.

브라우저와 보조 기술은 요소의 의미에 맞춰 초점과 활성화 동작을 제공한다.
그래서 divspan에 click 이벤트만 붙여 버튼처럼 만드는 방식은 추가 작업이 많이 필요하다.

<div class="button" onclick="save()">저장</div>

위 요소는 기본적으로 키보드 초점도 받지 않고 버튼 역할도 전달하지 않는다.
tabindex, keyboard event, ARIA role을 모두 직접 보완하는 것보다 네이티브 button을 사용하는 편이 안정적이다.

<button type="button" onclick="save()">저장</button>

HTML 요소의 기본 의미를 활용하면 JavaScript 코드도 줄고 접근성도 자연스럽게 따라온다.

링크 텍스트와 버튼 이름

링크 텍스트는 어디로 이동하는지 드러내야 한다.

<a href="/pricing">요금제 안내 보기</a>

여기, 자세히처럼 문맥을 떼면 목적지를 알기 어려운 표현은 반복해서 사용하지 않는 편이 좋다.
보조 기술 사용자는 페이지의 링크 목록만 따로 탐색할 수 있기 때문이다.

버튼 텍스트는 어떤 동작을 실행하는지 드러내야 한다.

<button type="button">댓글 삭제</button>

아이콘만 있는 버튼에도 접근 가능한 이름이 필요하다.

<button type="button" aria-label="검색창 닫기">
  <svg aria-hidden="true" viewBox="0 0 24 24">
    <!-- icon path -->
  </svg>
</button>

화면에 보이는 아이콘은 숨기지 않더라도, 보조 기술이 버튼 목적을 알 수 있는 텍스트나 aria-label을 제공해야 한다.

새 탭 링크

외부 문서를 새 탭에서 열어야 한다면 anchor의 target을 사용할 수 있다.

<a
  href="https://developer.mozilla.org/"
  target="_blank"
  rel="noopener noreferrer"
>
  MDN Web Docs 새 탭에서 열기
</a>

새 탭이나 새 창이 열리는 동작은 사용자에게 예상하지 못한 변화가 될 수 있다.
링크 텍스트나 아이콘을 통해 새 탭에서 열린다는 사실을 알리는 편이 좋다.

현재 브라우저는 target="_blank" 링크를 안전하게 다루는 기본 동작을 강화해 왔다.
그래도 rel을 명시하면 외부 문서와의 관계와 의도가 코드에 드러난다.

모든 외부 링크를 무조건 새 탭으로 열 필요는 없다.
사용자가 현재 문맥에서 자연스럽게 이동할 수 있다면 일반 링크 동작을 유지하는 편이 낫다.

다운로드 링크

파일을 내려받는 이동도 anchor가 담당할 수 있다.

<a href="/files/guide.pdf" download>가이드 PDF 다운로드</a>

이 링크는 다운로드할 실제 resource URL을 가진다.
따라서 다운로드 버튼처럼 보여도 의미상 anchor가 자연스럽다.

서버에서 파일을 생성하는 요청을 먼저 보내야 하거나, 현재 화면의 데이터를 가공해 파일을 만드는 복잡한 동작이라면 button으로 시작할 수도 있다.
핵심은 클릭 결과가 이미 존재하는 resource로 이동하는지, 현재 애플리케이션에서 새 동작을 실행하는지다.

disabled 상태의 선택

버튼은 disabled를 사용해 동작을 막을 수 있다.

<button type="submit" disabled>필수 항목을 입력하세요</button>

링크에는 네이티브 disabled 상태가 없다.
이동할 수 없는 상태라면 링크를 억지로 비활성화하기보다 왜 목적지가 아직 제공되지 않는지 UI 구조를 다시 보는 편이 좋다.

필요하다면 링크 대신 현재 상태를 나타내는 텍스트를 렌더링할 수 있다.

<span aria-current="page">결제 완료</span>

aria-disabled="true"만 anchor에 추가해도 클릭과 키보드 동작이 자동으로 막히지는 않는다.
ARIA는 의미를 전달하지만 네이티브 행동을 대신 구현해 주지 않는다.

요소 중첩의 주의점

링크 안에 버튼을 넣거나 버튼 안에 링크를 넣으면 안 된다.

<!-- 피해야 하는 구조 -->
<a href="/products/42">
  상품 보기
  <button type="button">좋아요</button>
</a>

두 interactive element가 겹치면 클릭 대상과 키보드 초점의 의미가 불분명해진다.
카드 전체 이동과 카드 안의 좋아요 동작이 모두 필요하다면 두 요소를 형제 관계로 분리하는 편이 낫다.

<article class="product-card">
  <a href="/products/42">상품 보기</a>
  <button type="button">좋아요</button>
</article>

시각적으로 겹쳐야 한다면 HTML 구조는 분리한 채 CSS layout이나 position으로 배치할 수 있다.
CSS position relative와 absolute 차이에서 기준 요소를 잡는 방법을 정리했다.

같은 모양으로 스타일링하는 기준

링크를 버튼처럼 보이게 만들 수 있고 버튼을 텍스트 링크처럼 보이게 만들 수도 있다.
시각적 모양이 HTML 의미를 결정하지는 않는다.

.primary-action {
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  justify-content: center;
  padding: 0 16px;
  border-radius: 4px;
  background: #1769e0;
  color: #fff;
}
<a class="primary-action" href="/signup">회원가입 페이지로 이동</a>
<button class="primary-action" type="submit">회원가입 완료</button>

두 요소는 같은 디자인을 공유하지만 첫 번째는 이동이고 두 번째는 form 제출이다.

클릭 영역을 키우는 padding과 요소 바깥 간격의 차이는 CSS margin과 padding 차이에서 확인할 수 있다.
inline 요소의 크기 동작은 CSS display block inline inline-block 차이와 연결해서 보면 좋다.

선택 기준

다른 페이지, 문서, 파일, 이메일 주소, 전화번호처럼 실제 목적지가 있다면 <a>를 사용한다.

<a href="/account/settings">계정 설정으로 이동</a>

현재 화면에서 열기, 닫기, 저장, 삭제, 토글, 제출 같은 행동을 실행한다면 <button>을 사용한다.

<button type="button">설정 창 열기</button>

선택이 어려울 때는 클릭한 뒤 브라우저 주소가 의미 있는 목적지로 바뀌는지 생각하면 된다.
주소로 표현할 수 있고 공유하거나 새 탭에서 열 수 있어야 한다면 링크에 가깝다.
주소 이동 없이 애플리케이션 상태가 바뀐다면 버튼에 가깝다.

정리

<a>는 이동을 위한 HTML 요소다.
실제 href를 제공하면 브라우저의 새 탭 열기, 주소 복사, 북마크 같은 링크 기능을 사용할 수 있다.

<button>은 동작을 실행하는 HTML 요소다.
일반 동작은 type="button", form 제출은 type="submit"으로 목적을 명시하는 편이 안전하다.

링크를 버튼처럼 꾸미거나 버튼을 링크처럼 꾸밀 수는 있다.
하지만 CSS 모양과 HTML 의미는 별개다.

목적지가 있으면 <a>, 실행할 동작이 있으면 <button>이다.
이 기준을 지키면 브라우저 기본 기능과 키보드 접근성을 적은 코드로 함께 얻을 수 있다.

참고 자료

관련 포스트
HTTP GET과 POST 차이 thumbnail
HTTP GET과 POST 차이
HTTP GET과 POST의 차이를 초보자 기준으로 정리합니다. 조회와 변경, query string과 request body, 캐시, safe, idempotent, form과 fetch 사용 기준을 함께 봅니다.
npm outdated와 npm update 차이 thumbnail
npm outdated와 npm update 차이
npm outdated와 npm update의 차이를 초보자 기준으로 정리합니다. Current, Wanted, Latest의 의미와 semver 범위 안에서 업데이트되는 방식, package-lock.json 변화까지 함께 봅니다.
npm install -g와 npx 차이 thumbnail
npm install -g와 npx 차이
npm install -g와 npx의 차이를 초보자 기준으로 정리합니다. 전역 설치, 로컬 설치, 일회성 실행, 프로젝트 scripts에 넣는 기준까지 함께 봅니다.
package.json에서 ^와 ~ 차이 thumbnail
package.json에서 ^와 ~ 차이
package.json 의존성 버전 앞에 붙는 ^와 ~의 차이를 초보자 기준으로 정리합니다. semantic versioning, 버전 범위, 0.x 예외, package-lock.json과의 관계까지 함께 봅니다.
npx와 npm exec 차이 thumbnail
npx와 npm exec 차이
npx와 npm exec가 어떤 명령어인지 초보자 기준으로 정리합니다. 로컬 패키지 실행, 원격 패키지 임시 실행, --package 옵션, -- 인자 전달 차이까지 함께 봅니다.
npm scripts와 npm run 정리 thumbnail
npm scripts와 npm run 정리
npm scripts란 무엇인지, package.json scripts와 npm run의 관계를 초보자 기준으로 정리합니다. npm run dev, npm start, npm test, node_modules/.bin, -- 인자 전달 방식까지 함께 봅니다.
dependencies와 devDependencies 차이 thumbnail
dependencies와 devDependencies 차이
package.json의 dependencies와 devDependencies 차이를 초보자 기준으로 정리합니다. npm install과 npm install -D, 배포 환경 설치, package-lock.json과의 관계까지 함께 봅니다.
npm ci와 npm install 차이 thumbnail
npm ci와 npm install 차이
npm ci와 npm install의 차이를 초보자 기준으로 정리합니다. clean install의 의미, package-lock.json 조건, CI에서 npm ci를 쓰는 이유, .npmrc 플래그 주의점까지 함께 봅니다.
.npmrc 파일이란? thumbnail
.npmrc 파일이란?
storybook을 사용해보려고 하다 마주한 이슈의 해결법을 알아보다가 등장한 .npmrc라는 파일에 대해 공부해보았다. .npmrc 파일이란? .npmrc 파일은 npm에 대한 config 파일이다. (npm에 대한 rc 파일) 프로젝트별 registry, install 옵션, 인증 토큰처럼 npm CLI가 읽는 설정을 관리할 때 사용한다.
스토리북이란? thumbnail
스토리북이란?
Storybook은 UI 컴포넌트를 독립적으로 개발하고, 문서화하고, 테스트하기 위한 프론트엔드 워크샵입니다. Storybook 10.4 기준 설치 흐름과 stories, 문서화, 테스트 활용 방식을 정리합니다.
TTV와 TTI 차이 thumbnail
TTV와 TTI 차이
TTV와 TTI의 차이를 초보자 기준으로 정리합니다. 사용자가 화면을 보는 시점, 상호작용 가능한 시점, FCP, LCP, INP, Core Web Vitals와의 관계까지 함께 봅니다.
Maria DB 외부 접속 설정하기 thumbnail
Maria DB 외부 접속 설정하기
안녕하세요. 오늘은 Maria DB 초기 세팅 시, 외부에서 접속이 안될 때 매뉴얼을 작성해보겠습니다. dotenv 패키지를 통해서 환경변수로 관리한다면, 별도의 추가 작업을 할 일이 없으실 겁니다.
package-lock.json 파일의 역할 thumbnail
package-lock.json 파일의 역할
안녕하세요. 오늘은 node 환경의 개발자라면 한번쯤 궁금했을만한 package-lock.json의 역할에 대해 알아보겠습니다. 우리는 node 환경에서 개발을 할 때 다양한 패키지들을 설치하여 활용하곤 합니다. 우리가 설치하는 패키지 또한 다른 npm 패키지를 활용하여 만든 패키지들이고 이들 또한 설치를 하게 됩니다. 이렇게 직간접적으로 설치된 패키지들은 대부분 호환성을 "^1.1.5"와 같이 표현하여, 범위로 지정해두고 있습니다.
Linux 환경 배포 자동화 체험해보기 thumbnail
Linux 환경 배포 자동화 체험해보기
안녕하세요. 요즘 포트폴리오를 만들면서 서버 상에 자주 반영할 일이 생겼는데, 매번 명령어들을 타이핑하는 것이 비효율적이라 생각이 들어 배포 자동화를 생각해보게 되었습니다. 현재 레벨에서는 단순히 명령어들만 단축시켜도 효율적이라 생각이 들어 간단한 쉘 스크립트만 작성하였습니다. 정말 간단하니 여러분도 도전해보시길 바랍니다.
협업 필수품. Prettier thumbnail
협업 필수품. Prettier
안녕하세요. 오늘은 Prettier이라는 도구에 대해 알려드리고자 합니다. 개발자는 각자의 코딩스타일이 존재합니다. 그러다보니 같은 프로젝트에서도 작성하는 소스마다 스타일이 제각기 다르기 일쑤입니다. 그럴 때 도입하면 좋은 것이 Prettier입니다. 프로젝트 root 폴더에 .prettierrc 라는 파일을 생성한 뒤, 위 예시와 같이 원하는 옵션을 JSON 형식으로 작성해주면 됩니다.