대부분 잊혀졌던 `<output>` 태그는 동적 결과를 기본적으로 스크린 리더에 알려주는, 오래전부터 사양에 있던 접근성 보석입니다. 이 글은 `<output>`의 동작 원리와 사용법, 주의사항, 그리고 React 등에서의 실제 활용 예제를 소개합니다.
모든 개발자는 <input>을 안다. 웹의 일꾼이다.
하지만 <output>은? 대부분 한 번도 써보지 않았다. 존재조차 모르는 이들도 있다.
안타깝다. 우리는 그동안 <div>와 ARIA로 땜질해 온 것을, 사실은 기본값으로 스크린 리더에 안내되는 “동적 결과”라는 요구를 이 태그가 해결해 주기 때문이다.
이 태그는 수년 전부터 사양에 있었다. 그런데도 눈앞에 있으면서도 숨어 있었다.
여기 HTML5 사양은 이렇게 말한다:
<output>요소는 애플리케이션이 수행한 계산 결과 또는 사용자 동작의 결과를 나타낸다.
이는 접근성 트리에서 role="status"로 매핑된다. 쉽게 말해, 값이 바뀌면 마치 이미 aria-live="polite" aria-atomic="true"를 갖고 있는 것처럼 그 변경 사항을 읽어 준다는 뜻이다.
실제로는 업데이트가 사용자를 방해하지 않는다. 잠시 후 읽어 주며, 변경된 부분만이 아니라 전체 내용을 읽어 준다. 필요하다면 직접 ARIA 속성을 지정해 이 동작을 재정의할 수 있다.
사용법은 아주 간단하다:
<output>여기에 동적 값이 표시됩니다</output>
이게 전부다. 보조공학 지원이 내장되어 있다. 외워야 할 속성도 없다. HTML이 원래 하도록 설계된 일을 할 뿐이다.
여러 단계를 거치는 폼이 있는 접근성 프로젝트를 하다가 <output>을 발견했다. 폼의 필드가 바뀔 때마다 위험 점수가 갱신되었다. 브라우저에서 보기에는 완벽했지만, 스크린 리더 사용자는 점수가 업데이트되는지 전혀 알 수 없었다.
ARIA 라이브 리전을 추가했더니 해결되었다. 하지만 나는 언제나 먼저 시맨틱 HTML을 선호했고, 라이브 리전은 종종 임시방편처럼 느껴진다.
그래서 사양을 샅샅이 뒤지다가 <output>이 눈에 들어왔다. 폼이 없어도 폼을 이해하고, 변경 사항을 네이티브로 안내한다. 가장 단순한 해결책이 처음부터 사양에 있었던 것이다.
우리가 잊었기 때문이다. 대부분의 튜토리얼에서 다루지 않는다. 화려해 보이지도 않는다. GitHub 공개 저장소를 검색해 보니 거의 쓰이지도 않았다.
패턴과 컴포넌트 라이브러리에서도 종종 빠져 있다. 이러한 부재는 피드백 루프를 만든다. 아무도 가르치지 않으니 아무도 사용하지 않는다.
<label>처럼, <output>에도 for="" 속성이 있다. 여기에는 결과가 의존하는 모든 <input> 요소의 id를 공백으로 구분해 나열한다:
<input id="a" type="number"> +
<input id="b" type="number"> =
<output for="a b"></output>
대부분의 사용자에게는 시각적으로 달라지는 것이 없다. 하지만 접근성 트리에서는 의미론적 연결이 생겨, 보조공학 사용자들이 입력값과 계산된 결과를 연관 지어 이해할 수 있다.
<form>도 필요 없다. 사용자의 입력에 따라 페이지의 동적 텍스트를 업데이트하는 어디에서든 사용할 수 있다.
기본적으로 <output>은 인라인 요소다. 보통은 <span>이나 <div>를 다루듯 레이아웃에 맞게 스타일링해 주면 된다.
그리고 2008년부터 사양의 일부였기 때문에, 브라우저와 스크린 리더 전반에서 지원이 훌륭하다. React나 Vue 같은 어떤 자바스크립트 프레임워크와도 잘 어울린다.
업데이트 2025년 10월 7일: 일부 스크린 리더가 이 태그의 업데이트를 읽어 주지 않는 것으로 보고되었다. 따라서 지원이 개선될 때까지는 role 속성을 명시적으로 강조하는 것이 유용할 수 있다: <output role="status">.
한 가지 주의할 점: <output>은 사용자 입력과 동작에 연동된 “결과”를 위한 것이다. 토스트 메시지 같은 전역 알림에는 적합하지 않다. 그런 경우에는 계산된 출력이 아니라 시스템 피드백을 나타내므로, 일반 요소에 role="status"나 role="alert"를 사용하는 편이 더 낫다.
그렇다면 실제로는 어떻게 보일까?
발견 이후 나는 여러 실제 프로젝트에서 <output>을 활용해 왔다:
최근 20분 코딩 챌린지에서, 계산 결과 표시를 <output>으로 처리했다. ARIA 역할을 단 하나도 추가하지 않았는데도 스크린 리더가 결과가 갱신될 때마다 알아서 읽어 주었다. 꼼수는 필요 없었다.
볼보카즈에서는 슬라이더 값을 사용자 친화적으로 보여 주었다. 내부적으로는 10000이더라도 출력에는 10,000 miles/year로 표시했다. 우리는 슬라이더와 <output>을 role="group"과 공유 레이블이 있는 컨테이너로 감싸, 응집력 있는 React 컴포넌트를 만들었다:
<div role="group" aria-labelledby="mileage-label">
<label id="mileage-label" htmlFor="mileage">
연간 주행거리
</label>
<input
id="mileage"
name="mileage"
type="range"
value={mileage}
onChange={(e) => setMileage(Number(e.target.value))}
/>
<output name="formattedMileage" htmlFor="mileage">
{mileage.toLocaleString()} 마일/년
</output>
</div>
비밀번호 강도 표시와 실시간 검증 메시지는 <output>과 정말 잘 맞는다.
<label for="password">비밀번호</label>
<input type="password" id="password" name="password">
<output for="password">
비밀번호 강도: 강함
</output>
<output> 태그는 API에서 가격을 받아 오거나, 세금 계산을 보여 주거나, 서버가 생성한 추천을 표시하는 최신 패턴에도 꼭 맞는다.
아래는 배송비 계산기가 <output> 태그를 업데이트해, 비용이 계산되면 사용자에게 알려 주는 예시다:
export function ShippingCalculator() {
const [weight, setWeight] = useState("");
const [price, setPrice] = useState("");
useEffect(() => {
if (weight) {
// 포장 무게에 따라 서버에서 배송비를 가져온다
fetch(`/api/shipping?weight=${weight}`)
.then((res) => res.json())
.then((data) => setPrice(data.price));
}
}, [weight]);
return (
<form>
<label>
포장 무게(kg):
<input
type="number"
name="weight"
value={weight}
onChange={(e) => setWeight(e.target.value)}
/>
</label>
<output name="price" htmlFor="weight">
{price ? `예상 배송비: $${price}` : "계산 중..."}
</output>
</form>
);
}
제대로 설계된 목적대로 네이티브 HTML 요소를 사용하는 일은 무척 만족스럽다. 특히 더 적은 코드로 UI 접근성을 높일 수 있다면 더더욱 그렇다.
<output>은 HTML의 가장 잘 숨겨진 비밀일지도 모른다. 이런 보석 같은 요소를 발견하는 일은, 사양 안에 아직도 얼마나 많은 가치가 숨어 있는지를 보여 준다.
가장 좋은 도구는, 정작 갖고 있는 줄도 몰랐던 그것일 때가 종종 있다.
업데이트 2025년 10월 11일: 늘 훌륭한 Bob Rudis가 이 글을 뒷받침하는 동작 예제 페이지를 만들어 주었다. 여기서 확인할 수 있다: https://rud.is/drop/output.html