openlang

저장됨이 두 번 읽히는 상태 영역

저장 한 번에 스크린 리더가 "저장됨"을 두 번 읽었어요. 뒤에 있는 상태는 saved로 가는 전이가 하나뿐이고, 리듀서 테스트도 그걸 보여 줘요.

두 번 바뀐 건 DOM이었어요. 상태 컴포넌트가 메시지가 있을 때만 영역을 그려요. 대략 message && <p aria-live="polite">{message}</p> 모양이라, 메시지 없음에서 Saved로 가면 글자가 이미 든 새 라이브 영역이 통째로 끼워져요. MDN 안내는 정반대예요. 빈 영역을 먼저 두고, 보조 기술에 노출될 시간을 준 다음 내용을 바꾸라고 하고, 브라우저와 스크린 리더 조합에 따라 동작이 다르다는 말도 붙어 있어요. 글자를 품은 채 나타나는 영역이 바로 그 경고하는 경우고, 제 환경에서는 두 번 읽기로 나왔어요.

그러니까 유니온은 맞았고 안내 음성은 틀렸는데, 타입 안의 무엇도 그걸 알려 줄 수 없었어요. 타입은 상태가 무엇인지를 말해요. 영역 노드가 다시 그려질 때 살아남는지는 DOM의 사실이고, 고칠 곳도 거기예요. 영역은 한 번만 그리고, 계속 붙여 두고, 안의 글자만 바꿔요.

이어지는 대화 3

서로의 근거를 따라
  1. 고유 링크

    저장을 눌렀어요. 이전 트리에는 영역이 없어요. 새 트리는 영역과 글자를 한 번에 들고 와요. 같은 태스크, 같은 변경 묶음이에요. 스크린 리더는 새 영역과 그 내용을 같은 순간에 받고, 그걸 어떻게 처리할지는 리더 마음이에요.

    그래서 MDN 조언이 이벤트 루프 지시처럼 읽혀요. 영역은 비워서 만들고 내용은 나중 태스크에서 넣으라고 하고, 스크립트로 만드는 영역이라면 대놓고 setTimeout()을 권해요. 노드를 계속 붙여 두면 같은 효과를 공짜로 얻어요. 첫 메시지보다 한참 전에 영역이 등록돼 있으니까요.

    왜 0번이 아니라 두 번이었는지는 저도 모르겠어요. 그 출발점이면 둘 다 그럴듯해요. 고친 방식이 그 질문을 아예 없애니까, 저라면 더 쫓지 않을래요.

  2. 고유 링크

    이제 영역은 계속 붙어 있어요. 한 번 저장하면 Saved가 떠요. 필드 하나 고치고 또 저장하면 글자는 또 Saved예요. 같은 문자열, 같은 노드. 리더 입장에선 아무것도 안 바뀐 건데, 이것도 읽어 주긴 하나요? 그리고 비웠다가 나중 태스크에서 다시 넣는 게 요령이라면, 그게 tess.p 님한테 두 번 읽기를 줬던 그 두 단계 모양이랑 같은 거 아닌가요?

  3. 고유 링크

    질문이 두 개 들어 있는데, 첫 번째는 스크린 리더가 끼기도 전에 React가 답해 버립니다. 컴포넌트가 Saved를 다시 Saved로 렌더링하면 React는 같은 텍스트라고 보고 DOM을 건드리지 않습니다. 변경이 없으니 리더가 알아챌 것도, 읽어 줄 것도 없습니다.

    비웠다가 다시 채우는 요령은 이 스레드에 나온 어떤 프레임워크보다 오래됐습니다(jQuery 시절 습관으로, .text('')로 비우고 setTimeout으로 문구를 되돌려 놓는 방식이었는데, 어느 리더 때문에 생긴 건지 아무도 기억하지 못하게 된 뒤에도 한참 복사돼 돌아다녔습니다). 그리고 tess.p가 겪은 두 번 읽기와는 모양이 다릅니다. 그쪽은 영역이 텍스트를 품은 채 새로 들어왔습니다. 여기서는 영역이 처음부터 등록돼 있고, 비우는 단계는 제거이며, aria-relevant 기본값이 additions text인 이상 제거는 읽으라고 만든 변경이 아닙니다. 두 번째 단계는 평범한 텍스트 변경입니다.

    다만 그 과정을 거친 뒤 같은 문구를 다시 읽어 주는지는 여전히 리더마다 다르니, 신경 쓰는 리더로 직접 들어 보고 믿는 편이 낫습니다.

같은 질문의 다른 갈래

  1. 체력바 하나를 빨갛게 했더니 전부 빨개졌다

    이번엔 Godot UI에서 공유 리소스에 당했어요. 작은 프로토타입의 유닛마다 체력바 씬이 붙어 있는데, 채움 부분에 StyleBoxFlat 오버라이드를 건 ProgressBar예요. 체력이 25 아래로 떨어지면 그 바 하나…

  2. 타일을 거절하면 팔레트 위치도 넘어가야 할까요?

    Lua에서 코루틴을 만들기만 해서는 아직 아무것도 돌지 않아요. 호스트가 coroutine.resume을 불러야 생성기가 돌기 시작하고, 제안을 명시적으로 yield하는 지점까지 가요. 한 번에 타일 하나씩이에요. 빨간 타일…

  3. 제출한 뒤 Vulkan 버퍼는 누가 소유하나요?

    Vulkan 버퍼를 감싼 C++ RAII 래퍼가 그 버퍼를 쓰는 작업을 기록해 제출하고 스코프를 벗어난다. 소멸자는 그 자리에서 vkDestroyBuffer를 호출한다. 버퍼를 참조하는 제출된 명령은 아직 실행 중이다. 이…

    답글 5cppc