HTML로 동적인 UI를 만들 때 대부분 이렇게 시작한다.
<input><div> 또는 <span> + aria-live 패턴하지만 HTML에는 “결과 출력”을 위해 태생적으로 설계된 태그가 이미 있다.
바로 거의 아무도 쓰지 않는 <output>이다.
공식 정의는 이렇다.
<output>요소는 애플리케이션이 수행한 계산 결과나
사용자의 액션 결과를 표현하는 요소이다.
핵심은 하나:
값이 바뀌었을 때, 스크린 리더에 자동으로 “변경된 결과”를 읽어준다.
<output>가 왜 중요한가?일반적으로 우리는 이런 패턴을 쓴다.
<div aria-live="polite" aria-atomic="true">
결과: 42
</div>
접근성 관점에서 이건 “라이브 영역(ARIA live region)” 패턴이다.
문제는:
aria-live, role, aria-atomic을 기억해서 달아야 하고div로 표현한다는 점에서 비표준적<output>은 여기서 깔끔하게 역할을 정리한다.
role="status"로 매핑aria-live="polite" aria-atomic="true"와 유사하게 동작즉, 제대로 된 의미론 + 접근성을 HTML 기본 기능만으로 얻을 수 있다.
가장 기본적인 형태는 이거다.
<output>여기에 동적인 값이 들어갑니다</output>
자바스크립트로 값을 갱신하면 된다.
<p>
결과:
<output id="result">0</output>
</p>
<script>
const result = document.getElementById("result");
result.textContent = "42"; // 값이 바뀌면 스크린 리더가 읽어줄 수 있음
</script>
특징:
inline (거의 <span>처럼 동작)<output>의 for 속성 – “이 결과는 무엇의 함수인가?”<label>에 for=""가 있는 것처럼,
<output>에도 for 속성이 있다.
<input id="a" type="number"> +
<input id="b" type="number"> =
<output for="a b"></output>
for="a b" → 이 결과는 id="a"와 id="b"에 의존한다는 의미폼(<form>) 안에 없더라도 사용 가능하다.
그냥 입력과 결과 관계를 의미적으로 표현하는 역할이라고 보면 된다.
<output>과 접근성 동작요약하면 <output>은 기본적으로 다음과 같이 행동한다.
role="status"로 취급aria-live="polite" aria-atomic="true" 조합을 기본 탑재한 느낌)다만 2025년 10월 기준으로:
일부 스크린 리더는
<output>변경을 제대로 읽지 않는 사례가 있다.
그래서 당분간은 다음처럼 role을 명시하는 것이 권장된다.
<output role="status">계산 결과가 여기에 표시됩니다</output>
role="status"로 의도를 명확히 표현하는 것이 안전하다.<output>을 쓰면 좋은 상황 vs 쓰면 안 되는 상황“사용자 입력/행동에 직접적으로 연결된 결과 값”을 보여줄 때
예를 들어:
10000, 보여주는 값: 10,000 miles/year이런 건 모두 “입력을 반영한 계산 결과” 이므로 <output>에 딱 맞는다.
“글로벌 알림, 토스트, 시스템 메시지” 같은 경우
예:
이런 경우는 사용자 입력의 계산 결과라기보다 시스템 피드백에 가깝기 때문에:
role="status"나 role="alert"를 가진 <div> 또는 <p>가 더 적합하다.가장 기본적인 “계산 결과” 사례.
<label>
A:
<input id="a" type="number" value="0">
</label>
<label>
B:
<input id="b" type="number" value="0">
</label>
<p>
합계:
<output id="sum" for="a b" role="status">0</output>
</p>
<script>
const a = document.getElementById("a");
const b = document.getElementById("b");
const sum = document.getElementById("sum");
function updateSum() {
const av = Number(a.value) || 0;
const bv = Number(b.value) || 0;
sum.textContent = av + bv;
}
a.addEventListener("input", updateSum);
b.addEventListener("input", updateSum);
</script>
슬라이더 내부 값은 “10000”,
사용자에게는 “10,000 miles/year” 형태로 보여주고 싶을 때.
<div role="group" aria-labelledby="mileage-label">
<label id="mileage-label" htmlFor="mileage">
Annual mileage
</label>
<input
id="mileage"
name="mileage"
type="range"
value={mileage}
onChange={(e) => setMileage(Number(e.target.value))}
/>
<output
name="formattedMileage"
htmlFor="mileage"
role="status"
>
{mileage.toLocaleString()} miles/year
</output>
</div>
포인트:
role="group" + aria-labelledby로 묶인 컴포넌트로 인식<output>이 slider 값의 “읽기 좋은 표현” 역할<label for="password">Password</label>
<input type="password" id="password" name="password">
<output id="password-strength" for="password" role="status">
Password strength: Unknown
</output>
<script>
const input = document.getElementById("password");
const output = document.getElementById("password-strength");
function getStrength(pw) {
if (pw.length < 6) return "Weak";
if (!/[0-9]/.test(pw)) return "Medium";
return "Strong";
}
input.addEventListener("input", () => {
const strength = getStrength(input.value);
output.textContent = `Password strength: ${strength}`;
});
</script>
이런 식의 “실시간 메시지”는
기존에는 aria-live를 붙인 div/span으로 구현하던 전형적인 패턴이다.
<output> 하나로 의미 + 접근성을 동시에 해결할 수 있다.
export function ShippingCalculator() {
const [weight, setWeight] = useState("");
const [price, setPrice] = useState("");
useEffect(() => {
if (!weight) {
setPrice("");
return;
}
fetch(`/api/shipping?weight=${weight}`)
.then((res) => res.json())
.then((data) => setPrice(data.price))
.catch(() => setPrice("error"));
}, [weight]);
return (
<form>
<label>
Package weight (kg):
<input
type="number"
name="weight"
value={weight}
onChange={(e) => setWeight(e.target.value)}
/>
</label>
<output name="price" htmlFor="weight" role="status">
{price === "error"
? "Failed to calculate shipping."
: price
? `Estimated shipping: $${price}`
: "Calculating..."}
</output>
</form>
);
}
<output>에 자연스럽게 들어간다.<output>의 호환성과 주의사항role="status"를 명시하는 게 안전한 선택<output role="status">...</output>
inline이므로 필요하면 CSS로 스타일링output {
display: inline-block;
font-weight: 600;
}
<output>은 화려하지도 않고,
튜토리얼에도 잘 나오지 않는다.
그래서 대부분의 코드베이스는 지금도:
<div aria-live="polite">…</div><span role="status">…</span>같은 패턴으로 “결과 값”을 구현한다.
하지만 역할과 의미만 놓고 보면:
<output>role="status" 또는 role="alert"가 붙은 요소이렇게 나누는 편이 훨씬 깔끔하다.
“이미 스펙에 있었는데, 우리가 잊고 있었던 기능”에 가깝다.
접근성을 챙겨야 하는 대규모 폼, 대시보드, SaaS, 관리 도구를 만든다면
지금 사용하는 “실시간 결과 UI” 중 일부를 <output>으로 바꾸는 것만으로도
코드는 단순해지고 의미는 더 정확해진다.