요즘 웹은 페이지를 새로 부르지 않고 내용만 바꾸는 일이 많다. 저장이 완료되고, 검색 결과 수가 바뀌고, 알림이 뜨고, 오류가 나타난다. 눈으로 보는 사람은 이 변화를 즉시 알아챈다. 그런데 화면 낭독기 사용자는 어떨까. 이번 편은 새로고침 없이 바뀌는 내용을 낭독기 사용자에게도 전하는 동적 콘텐츠 알림을 다룬다.


나는 저장 버튼을 누르면 저장되었다는 문구가 뜨는 화면을 만들었다. 눈으로 보는 사람에게는 완벽했다. 그런데 낭독기로 들어보니, 그 문구가 뜨는데도 낭독기는 아무 말이 없었다. 낭독기 사용자는 저장이 됐는지 안 됐는지 알 수 없었던 것이다. 그 순간 나는 화면에 뜬 것이 곧 낭독되는 것은 아니라는 사실을 배웠다.

1. 변화는 알려야 전해진다

화면 낭독기는 페이지를 위에서 아래로 한 번 읽고 나면, 그 뒤에 바뀐 내용을 자동으로 다시 읽지 않는다. 사용자가 이미 다른 곳을 듣고 있는데, 저 아래 어딘가에서 문구가 바뀐들 낭독기는 그것을 알지 못한다. 나는 이 특성을 이해하고 나서, 동적으로 바뀐 내용은 따로 알려주지 않으면 낭독기 사용자에게 전해지지 않는다는 걸 알았다.


눈은 화면 전체를 늘 훑고 있어서 어디가 바뀌든 금세 알아챈다. 화면 구석에 작은 알림이 떠도 눈은 그것을 잡는다. 그러나 낭독기는 한 번에 한 곳만 듣기에, 지금 듣는 곳 바깥의 변화를 놓친다. 나는 이 차이를 늘 염두에 두고, 눈으로만 확인되는 변화가 낭독기 사용자에게도 전해지는지를 따로 챙기게 됐다.


알려야 할 변화는 생각보다 많다. 저장이나 삭제의 완료, 검색 결과 개수의 갱신, 장바구니에 담김, 오류 메시지의 등장, 새 알림의 도착이 모두 여기 해당한다. 나는 화면에서 무언가 바뀌는 자리를 발견하면, 이 변화가 낭독기 사용자에게도 전해져야 하는지를 먼저 묻는다. 대개는 전해져야 하고, 그렇다면 알림 처리가 필요하다.


변화를 안 알리면 낭독기 사용자는 방금 무슨 일이 일어났는지 몰라 헤맨다. 버튼을 눌렀는데 아무 반응이 안 들리면, 눌린 건지 안 눌린 건지 몰라 다시 누르게 된다. 나는 이런 상황이 낭독기 사용자를 얼마나 답답하게 하는지 겪어보고, 모든 중요한 변화에 소리 반응을 붙이는 것을 원칙으로 삼았다. 반응은 곧 안내다.


그렇다고 모든 변화를 다 알릴 필요는 없다. 사소한 변화까지 일일이 낭독하면 사용자가 피로해진다. 나는 이 변화를 못 들으면 사용자가 곤란해질까를 기준으로, 알릴 것과 넘어갈 것을 가른다. 중요한 변화는 또렷이 알리고, 자잘한 것은 조용히 지나가게 한다. 절제와 안내의 균형을 잡는 것이 동적 알림의 요령이다.


정리하면 낭독기는 첫 낭독 뒤의 변화를 자동으로 읽지 않으므로, 중요한 변화는 따로 알려야 하고, 눈으로만 확인되는 변화가 낭독기에도 전해지는지 챙기되, 사소한 것까지 남발하지는 않는다. 나는 변화에 소리 반응을 붙인다. 그렇다면 변화를 낭독기에 알리는 도구는 무엇일까. 그것이 라이브 리전이다.

2. 라이브 리전이라는 도구

동적 변화를 낭독기에 알리는 도구가 라이브 리전이다. 어떤 영역을 라이브 리전으로 지정해 두면, 그 안의 내용이 바뀔 때 낭독기가 자동으로 그 변화를 읽어 준다. 사용자가 다른 곳을 듣고 있어도, 라이브 리전의 변화는 낭독기가 챙겨 전한다. 나는 이 도구를 변화를 낭독기에 실어 나르는 통로로 이해한다.


영역을 라이브 리전으로 지정하는 속성이 aria-live다. 이 속성을 붙인 영역은 내용이 바뀔 때마다 낭독기가 그 변화를 읽는다. 나는 저장 완료 문구나 검색 결과 수처럼 동적으로 바뀌는 내용을 담는 영역에 이 속성을 붙여, 그 변화가 소리로도 전해지게 한다. 이 속성 하나로 조용하던 변화가 낭독기에 말을 걸게 된다.


이 속성에는 긴급함의 정도를 정하는 두 가지 값이 있다. 하나는 정중함을 뜻하는 aria-live="polite"로, 낭독기가 지금 읽던 것을 마친 뒤에 변화를 알린다. 다른 하나는 단호함을 뜻하는 aria-live="assertive"로, 읽던 것을 끊고 즉시 변화를 알린다. 나는 변화의 긴급함에 따라 이 두 값을 가려 쓴다.


대부분의 변화에는 정중한 값을 쓴다. 저장 완료나 결과 갱신 같은 것은 급하지 않으니, 낭독기가 지금 하던 낭독을 끝낸 뒤 알려도 충분하다. 오히려 급하지 않은 것을 끼어들며 알리면 사용자의 흐름을 끊는다. 나는 알림의 기본값을 정중함으로 두고, 사용자가 지금 듣던 것을 방해하지 않으면서 변화를 전하게 한다.


단호한 값은 정말 급한 변화에만 아껴 쓴다. 진행을 막는 심각한 오류처럼 지금 당장 알려야 하는 것에만 읽던 것을 끊고 끼어든다. 나는 이 단호한 값을 남용하지 않으려 특히 조심한다. 급하지 않은 것까지 끼어들며 알리면, 낭독기 사용자는 계속 흐름이 끊겨 오히려 페이지를 쓰기 어려워진다. 단호함은 최후에만 쓴다.


정리하면 라이브 리전은 동적 변화를 낭독기에 실어 나르는 통로이고, 지정 속성으로 영역을 라이브로 만들며, 정중함과 단호함 두 값 중 대부분은 정중함을 쓰고 단호함은 급한 경우에만 아껴 쓴다. 나는 긴급함에 맞는 값을 고른다. 이 라이브 리전을 더 간편하게 지정하는 전용 역할도 있다.

3. 상태 역할과 경보 역할

라이브 리전을 간편하게 지정하는 두 가지 전용 역할이 있다. 하나가 상태 역할 role="status"다. 이 역할을 붙인 영역은 정중한 라이브 리전으로 동작해, 내용이 바뀌면 낭독기가 지금 낭독을 마친 뒤 알린다. 나는 저장 완료나 결과 갱신 같은 일반적인 알림에 이 상태 역할을 즐겨 쓴다. 속성을 따로 붙이지 않아도 정중한 알림이 된다.


다른 하나가 경보 역할 role="alert"다. 이 역할을 붙인 영역은 단호한 라이브 리전으로 동작해, 내용이 바뀌면 낭독기가 읽던 것을 끊고 즉시 알린다. 나는 진행을 막는 오류처럼 급히 알려야 하는 것에만 이 경보 역할을 쓴다. 상태 역할이 일상적인 알림이라면, 경보 역할은 정말 급할 때의 비상벨 같은 것이다.


이 두 역할은 라이브 속성을 직접 붙이는 것과 같은 효과를 낸다. 다만 역할 이름이 의미를 함께 드러내서, 코드를 읽는 사람도 이것이 상태 알림인지 경보인지 한눈에 안다. 나는 라이브 속성을 직접 쓰기보다 이 전용 역할을 즐겨 쓴다. 의도가 이름에 담겨 있어, 나중에 코드를 볼 때 무엇을 위한 영역인지 바로 알 수 있기 때문이다.


둘을 고르는 기준은 급함의 정도다. 지금 당장 사용자의 주의를 끊고 알려야 하면 경보 역할을, 하던 것을 방해하지 않고 알려도 되면 상태 역할을 쓴다. 나는 알림을 만들 때마다 이것이 흐름을 끊을 만큼 급한가를 묻는다. 대부분은 급하지 않아 상태 역할로 충분하고, 경보 역할은 손에 꼽을 만큼만 쓴다.


두 역할에 라이브 속성을 또 붙이지는 않는다. 경보 역할은 이미 단호한 라이브 리전이라, 거기에 단호한 라이브 속성을 다시 붙이면 낭독기에 따라 같은 내용을 두 번 읽을 수 있다. 나는 전용 역할을 쓸 때는 라이브 속성을 겹쳐 붙이지 않는다. 역할 하나로 이미 충분하니, 중복은 오히려 이중 낭독이라는 문제를 부른다.


정리하면 라이브 리전에는 정중한 상태 역할과 단호한 경보 역할이라는 전용 역할이 있어, 급함에 따라 골라 쓰고, 의도가 이름에 드러나며, 라이브 속성을 겹쳐 붙이지는 않는다. 나는 대부분 상태 역할을 쓰고 경보는 아껴 쓴다. 그런데 이 라이브 리전에는 반드시 지켜야 할 미묘한 규칙이 하나 있다.

4. 영역은 미리 존재해야 한다

라이브 리전에서 가장 자주 걸려 넘어지는 함정이 영역을 만드는 시점이다. 라이브 리전은 페이지가 뜰 때부터 비어 있는 채로 미리 있어야 하고, 나중에 그 안의 내용만 채워야 낭독된다. 요소를 새로 만들면서 동시에 내용까지 채우면, 낭독기가 변화를 알아채지 못하고 넘어가는 경우가 많다. 나는 이 규칙을 몰라 한참 헤맸다.


왜 그럴까. 낭독기는 이미 있던 영역의 내용이 바뀌는 것을 변화로 감지한다. 그런데 영역 자체가 내용과 함께 새로 나타나면, 낭독기가 그것을 변화로 잡아채기 전에 지나쳐 버릴 수 있다. 나는 이 원리를 이해하고 나서, 알림 영역을 페이지가 뜰 때 미리 빈 채로 만들어 두는 것을 습관으로 삼았다. 그릇을 먼저 놓고 나중에 담는 것이다.


그래서 나는 알림을 쓸 자리에 빈 라이브 리전을 미리 심어 둔다. 저장 알림을 위한 영역, 오류 알림을 위한 영역을 페이지 로드 시점부터 내용 없이 놓아둔다. 그리고 알림이 필요한 순간에 그 영역의 내용만 바꾼다. 이렇게 하면 낭독기가 변화를 확실히 감지해 읽는다. 그릇이 미리 있으니 무엇을 담아도 낭독기가 알아챈다.


내용을 채울 때는 텍스트만 갈아 끼운다. 영역은 그대로 두고 그 안의 글자만 새 내용으로 바꾸는 것이다. 나는 알림이 뜰 때마다 미리 있던 영역의 텍스트를 저장되었다거나 오류가 발생했다는 식으로 바꾼다. 영역은 페이지 내내 자리를 지키고, 그 안의 내용만 상황에 따라 바뀌며 낭독된다. 그릇은 고정, 내용은 유동이다.


이 규칙을 지키지 않으면 알림이 들쭉날쭉해진다. 어떤 때는 읽히고 어떤 때는 안 읽히는 불안정한 알림이 된다. 눈으로는 멀쩡히 뜨는데 소리로는 안 나오니, 원인을 찾기도 어렵다. 나는 알림이 가끔 안 읽히는 문제를 만나면, 먼저 영역이 미리 존재했는지부터 확인한다. 대개 이 시점 규칙을 어긴 것이 원인이었다.


정리하면 라이브 리전은 페이지가 뜰 때부터 빈 채로 미리 있어야 하고, 알림이 필요할 때 그 안의 텍스트만 바꿔야 확실히 낭독되며, 영역과 내용을 동시에 만들면 변화를 놓친다. 나는 그릇을 먼저 놓고 내용을 담는다. 마지막으로, 라이브 리전에서 흔히 저지르는 다른 실수들을 짚어 보자.

5. 흔한 실수를 피한다

라이브 리전에서 흔한 첫 번째 실수가 단호한 알림의 남용이다. 모든 알림을 끼어드는 방식으로 만들면, 낭독기 사용자는 계속 흐름이 끊겨 페이지를 쓰기 어려워진다. 나는 알림을 만들 때마다 이것이 정말 지금 당장 끼어들 만큼 급한지를 묻는다. 대부분은 그렇지 않아 정중한 알림으로 충분하다. 단호함은 정말 급한 경우의 예외다.


두 번째 실수가 알림 영역을 화면에서 완전히 숨기는 것이다. 화면에는 안 보이되 낭독기에는 읽혀야 하는 알림을 만들 때, 흔한 방식으로 완전히 감추면 낭독기에서도 사라져 읽히지 않는다. 나는 이런 알림에는 앞서 배운 시각적 숨김을 써서, 화면 밖으로 밀어내되 낭독기는 읽게 한다. 완전히 숨기면 알림이 침묵한다.


세 번째 실수가 같은 내용을 두 번 읽게 만드는 것이다. 전용 알림 역할에 라이브 속성을 겹쳐 붙이거나, 알림 영역을 잘못 다루면 낭독기가 같은 내용을 반복해 읽는다. 나는 알림이 두 번 읽히는 문제를 만나면, 속성이 겹쳐 있지 않은지부터 확인한다. 하나의 알림에는 하나의 지정이면 충분하고, 겹치면 반복이 된다.


네 번째로, 같은 내용을 다시 넣으면 낭독이 안 될 수 있다. 낭독기는 내용이 바뀌어야 변화로 감지하는데, 이미 있던 것과 똑같은 텍스트를 다시 넣으면 변화가 없다고 여겨 읽지 않는다. 나는 같은 알림을 연달아 띄워야 할 때, 잠시 영역을 비웠다가 다시 채우는 식으로 변화를 만들어 낭독기가 확실히 감지하게 한다.


다섯 번째로, 알림 영역을 실제 낭독으로 확인하지 않는 실수다. 이 모든 것은 눈으로만 봐서는 맞았는지 알 수 없다. 화면에는 알림이 잘 떠도 낭독은 안 될 수 있기 때문이다. 나는 알림을 만든 뒤 반드시 낭독기를 켜고 그 알림이 실제로 소리로 나는지 귀로 확인한다. 동적 알림만큼은 들어보지 않으면 절대 확신할 수 없다.


정리하면 라이브 리전에서는 단호한 알림의 남용, 완전한 화면 숨김, 이중 낭독, 같은 내용 반복, 낭독 미확인이라는 실수를 피해야 한다. 나는 이 함정들을 하나씩 점검하며 알림을 다듬는다. 지금까지 소리로 전하는 변화를 다뤘다면, 다음 편에서는 눈에 보이는 움직임을 배려하는 모션과 애니메이션을 다룬다.