접근성을 지원한다는 착각

접근성을 지원한다는 착각



접근성 지원을 한다고 하면, 사람들은 뭘 더 해야 한다고 생각합니다. 뭘 더 하는 만큼 개발기간도 더 늘어난다고 생각합니다. 하지만 이런 생각은, 보안성을 챙기면 개발기간이 늘어난다는 주장만큼이나 틀린 말입니다. 보안성을 고려하며 개발하는 것이지, 개발 다 끝내놓고 그 다음에 보안성을 따로 높이는 사람이 어디있나요? 접근성을 고려하는 일은, 사실 우리가 더 좋은 제품을 만들기 위해 늘 하는 고민들의 다른 측면일 뿐입니다.

하지만 평소에 접근성을 높이는 일에 관심이 없던 사람 입장에선, 이런 얘기도 뜬구름 잡는 얘기처럼 들립니다. 접근성 지원이 어떻게 우리가 늘 하는 고민의 다른 측면일 수 있을까요? 나는 그런 고민을 한 적이 없는데 말이죠.

관점을 바꿔, 이렇게 여쭤보겠습니다.
당신은 프로그래머로써 일반 텍스트(plain text)의 힘을 충분히 활용하고 있습니까?




일반 텍스트의 힘


『실용주의 프로그래머』는 일반텍스트(plain text)의 힘을 다음과 같이 강조합니다.

…우리는 지식을 다루기 가장 좋은 형식이 일반 텍스트라고 믿습니다. 일반 텍스트를 사용하면, 우리는 손으로든 프로그램으로든 거의 모든 도구를 사용해 지식을 조작할 수 있는 능력을 얻게 됩니다.

대부분의 바이너리 형식의 문제는, 데이터를 이해하는 데 필요한 문맥이 데이터 자체와 분리되어 있다는 점입니다. …(중략) 애플리케이션 없이 해석할 수 없다면, 그 데이터는 암호화된 것이나 다름없으며 전혀 의미를 지니지 못합니다.

사람이 읽을 수 있는 형태의 데이터, 그리고 자기 기술적(self-describing) 데이터는 그 데이터를 생성한 애플리케이션이나 그 외 모든 형식의 데이터를 뛰어넘어 더 오래 살아남을 것입니다. 단언컨대 그렇습니다. 데이터만 살아남는다면, 그것을 사용할 기회는 여전히 존재합니다 — 비록 그 데이터를 작성한 원래의 애플리케이션이 사라진 이후라도 말입니다.


접근성 높은 제품을 만든다는 것은, 더 다양한 환경에서 동작하는 제품을 만든다는 것과 정확히 같은 뜻입니다. 손이 없는 유저가 음성이나 눈으로 접근할 수 있는 환경, 눈이 없는 유저가 시각적 요소 없이 정보에 접근해야 하는 환경, 손과 눈이 모두 없는 컴퓨터가 어떻게든 정보에 접근해 단위테스트를 실행시켜야하는 환경에서 모두 동작하는 제품이 접근성 높은 제품입니다. 이 때 다양한 환경이 같은 출력물을 일관되게 해석할 수 있게 하기 위해선, 그 출력물은 반드시 텍스트여야 합니다. 오직 텍스트만이 특정 환경에 의존하지 않고 그 자체적으로 해석될 수 있기 때문입니다.

그래서 유능한 프로그래머는 언제나 ‘입력과 출력을 어떻게 텍스트로 다룰 수 있을까’를 고민합니다. 텍스트로만 만든다면, 그것이 무엇이든 분석하고 조작할 수 있기 때문입니다. 우리가 API와 CLI로 개발하는 이유, 가장 강력한 AI가 대형언어모델의 형태인 이유에는, 모두 같은 배경이 있는 것입니다.

접근성을 지원할 때, 우리가 하는 대부분의 일이 UI에 텍스트 형태의 정보를 추가하는 일인 것도, 결코 우연이 아닌 방금 말한 배경에서 나온 의도적 디자인입니다. 실제로, 우리가 UI에 입력한 aria-label, accessibilityLabel 등은 시각장애인용 스크린리더가 읽을 텍스트로서만 역할하지 않고, 음성명령 모드나 안구추적 모드에서는 시각적 형태로 각 UI의 경계와 의미를 표시하는 역할을 하게 됩니다. 생각해보면 놀랍지 않나요? iOS의 음성명령 모드는, accessibilityLabel이란 인터페이스가 만들어진지 10년 후에 등장한 기능입니다. 텍스트기반의 인터페이스는, 이렇게 10년뒤의 미래의 기술에조차 자연스럽게 녹아들 수 있는 것입니다.



1_01
iOS의 음성명령 모드는 10년 전에 만들어진 텍스트기반의 API인 accessibilityLabel을 여전히 십분 활용할 수 있습니다.


테스트코드는 우리가 그 텍스트 정보를 사용할 수 있는 또다른 인터페이스의 하나일 뿐입니다. 이 관점에 익숙해져버린 저는, 텍스트의 형태로 변환되지 않은 출력물에 대하여 어떻게 테스트코드를 작성하는지, 도저히 상상할 수 없는 지경에 이르렀습니다.





당신이 테스트코드를 작성하지 못하는 이유

그래서 접근성 지원을 등한시하는 개발자를 만나면, 저는 물어봅니다.
(도대체) 테스트코드는 어떻게 짜시나요?

그러면 거의 대부분의 개발자가 이렇게 답합니다.
UI는 테스트하지 않고, 비지니스 로직만 테스트합니다 라고요.

그런데 대체 비지니스 로직이 뭔가요? 그 정의상 ‘회사에 돈을 벌어다주는 로직’인 비지니스 로직에, 왜 UI 로직은 빠지는 것인가요? 당신이 작성하는 코드 대부분이 사실 UI를 그리는 코드 아닌가요? 그리고 실제로 더 좋은 사용자경험을 제공하는 UI를 만드는 것이, 회사가 돈을 버는 방식이 아닌가요?

원래 ‘UI를 테스트하지 말라’라는 것은, 사실 ‘입출력’을 테스트하지 말라는 말입니다. 가령 DB의 입출력은, 데이터를 SSD에 저장하냐 자기 테이프에 저장하냐 등에 따라 그 세부행동이 달라집니다. 같은 ReactNative 코드여도, iOS 기기에서 실행되느냐에 안드로이드 가상기기에서 실행되느냐에 따라 그 세부행동이 달라집니다. 그러나 세부행동이 다를 뿐이지, 실제로 만들어지는 결과물에는 차이가 없는 경우가 많습니다. 이렇게 ‘현실 물질 세계’와 인접한 영역인 입출력단은 유닛테스트가 실행될 수 있는 환경 밖을 테스트하므로 테스트 자체가 불가능하기도 하고, 억지로 가상의 테스트 환경을 만들더라도 그 복잡도에 비해 얻을 수 있는 실익도 적은 경우가 많습니다. 그런 것을 테스트하는 것은 그 입출력 장치를 만드는 제조사, 예컨대 애플이나 구글, 삼성같은 회사가 할 일이지요.

하지만 입출력의 레벨이 아닌, 그 직전단계에서 UI가 적절하게 구성되었는지, 이 버튼을 눌렀을 때 저 화면이 나오는지, 특정 상황에서 필요한 정보가 모두 화면에 노출되는지, 또는 노출되어선 안 되는 정보는 잘 감춰져 있는지 등은 가장 중요한 비지니스 로직입니다. 비지니스 로직은, 말하자면 제품요구사항 문서에 텍스트로 적힌 모든 요구사항입니다. 텍스트로 적힌 요구사항은 모두 텍스트 형태의 테스트코드의 형태로 번역될 수 있어야 합니다. 그 요구사항 중 테스트코드로 표현할 수 없는 요구사항이 있다면, 그것은 높은 확률로, 당신이 텍스트가 아닌, 화면이란 이름의 바이너리를 다루고 있기 때문입니다.





출력물이 텍스트일 때 테스트코드 작성은 비로소 가능해집니다

아주 단순한 예시를 들어보겠습니다.
해당 화면에 처음 진입했을 때, 첫번째 섹션은 접혀져 있어야 하고, 두번째 섹션은 펼쳐져 있어야 한다는 요구사항이 있다고 해봅시다. 이를 어떻게 테스트코드로 표현할 수 있을까요?

일단 이 ‘화면’이란 출력물을 대상으로 테스트하려면 막막합니다. ‘접힘’과 ‘펼침’을 어떻게 판별할 수 있을까요?


1_02
첫째 섹션은 접혀있고, 두번째 섹션은 펼쳐진 UI. 이 화면으로 출력된 UI를 텍스트 기반 인터페이스로 테스트하기란 어렵습니다.



화면의 ‘이미지’를 테스트하는 스냅샷테스트는 이론상 가능하지만, 그 테스트코드는 명세, 문서로서 전혀 기능하지 못하며 유지보수 비용 또한 큽니다. 텍스트형태의 요구사항에 포함되지 않은, 많은 사소한 디테일들을 한 번에 검사하기 때문입니다.

func TestSnapshot() {
  let view = MyView()
  let snapshot = view.snapshot
  assertEqual(snapshot, MyView.reference.png)
}
스냅샷테스트의 예시. 테스트코드를 읽어서는 뭘 테스트한다는건지 알 수 없습니다.


‘어떤 섹션이 접혀져있어야 하는지’를 ‘비지니스 로직’으로 떼어내어 테스트하는 방식으로는, 이렇게 작성할 수 있을 것 같습니다.

describe('expandedSectionsOnInitialRender') {
    const expandedSections = expandedSectionsOnInitialRender()
    it('첫번째 섹션은 접혀져 있어야 한다') {
        assert(expandedSections.not.to.contain(0))
    }
   it ('두번째 섹션은 펼쳐져 있어야 한다') {
      assert(expandedSections.to.beEqualTo([1])
   }
}

이것도 나쁘지는 않습니다. 하지만 이를 테스트하기 위해서, 어쩌면 굳이 필요하지 않았을 함수 하나를 더 만들어야 했죠. 경우에 따라서는 요구사항을 표현할 수 있는 새로운 상태를 표현할 새 자료형을 만들어야 했을 수도 있겠습니다. 변수하나라도, 이 접근은 테스트코드를 위한 코드 를 만들게 합니다.

무엇보다도, 이 접근의 가장 아쉬운 점은, 이렇게 만들어진 함수가, 실제 적재적소에 사용되고 있을지는 보장할 수 없다는 점입니다. 선언만 잘 선언되고, 테스트코드는 붙여졌지만, 어느새 누군가의 리팩토링에 의해 해당 함수의 사용처는 제거되고 함수 스스로만 남아 공허한 테스트코드와 코드베이스에 남아, ‘이 명세가 이렇게 존재하고 있다’고 거짓말을 하고 있는 상황이 발생할 수도 있습니다.

하지만 애초에 접근성을 지원한 화면이나 컴퍼넌트라면, 이렇게 테스트코드를 작성할 수 있습니다.

describe('고정비 상세', () => {
  render(
      <고정비상세화면 />,
    );

  it('첫번째 섹션은 접혀져 있어야 한다', async () => {
    expect(screen.getByRole('header', { name: '나간 고정 지출' })).toBeCollapsed();
  })
})

이 화면에서 우리는 헤더영역에 ‘header’ 라는 aria-role 을 부여했습니다. 그래서 스크린리더는 이 정보를 바탕으로 ‘머리말 단위의 네비게이션’을 가능하게 할 수도 있고, 테스트환경은 헤더 단위의 하위 컴포넌트를 쿼리할 수 도 있게 된 것입니다.

또한 해당 컴퍼넌트에 대하여 aria-expanded 속성을 부여했습니다. 그래서 스크린리더는 해당 영역이 접혀있는지를 바탕으로 적절한 네비게이션 경험을 제공할 수도 있고, 테스트환경은 해당 컴퍼넌트가 ‘접혀있는지’에 대해 이미지 없이 검사할 수 있게 된 것입니다.

이 테스트코드를 작성할 때에는, 테스트코드만을 위해 작성해야 할 코드는 아무것도 없습니다. 그저 출력물이 텍스트가 된 것만으로 우리는 함수든 자료형이든, 테스트코드를 위해서가 아닌 우리가 정말 필요할 때 만들 수 있게 되었습니다. 그리고 출력물 그 자체를 검사하기 때문에, 테스트코드란 이름의 명세가 실제 구현체의 행동과 어긋날 여지도 없습니다.

이러한 원리는 플랫폼을 가리지 않습니다. HTML처럼 원래 텍스트기반의 중간 출력물을 제공하던 웹 진영에선 이미 이런 종류의 테스트를 지원하는 도구들이 많이 있습니다. 하지만 iOS를 비롯해 어떤 환경이라 하더라도, 결국 텍스트기반의 ViewTree 형태의 출력물을 만드는 방법은 접근성을 조금만 고려해서 개발한다면 아주 손쉽게 고안할 수 있습니다. 뱅크샐러드에서 사용하는 AXSnapshot 같은 도구가 바로 그런 도구입니다.

func testMyViewController() async throws {
    let viewController = MyViewController()
    await viewController.doSomeBusinessLogic()
    
    XCTAssert(
        viewController.axSnapshot() == """
        ------------------------------------------------------------
        Final Result
        button, header
        Double Tap to see detail result
        Actions: Retry
        ------------------------------------------------------------
        The question is, The answer to the Life, the Universe, and Everything
        button
        ------------------------------------------------------------
        The answer is, 42
        button
        ------------------------------------------------------------
        """
    )
}
UI를 텍스트 형태로 표현해 테스트의 대상으로 삼는 전략은, 텍스트 자체의 접근성에 기대기 때문에 플랫폼을 가리지 않고 유효합니다.


당신이 이런 테스트코드를 짤 수 없었던 것은, 또는 테스트코드 작성을 위해 더 많은 추상화를 만들어야 했던 이유는, 그래서 결국 테스트에는 비용이 든다 고 판단하게 되었던 이유는, 당신이 당신의 출력물을 텍스트라는 인터페이스로 바라본 적이 없기 때문에, 그리고 그 인터페이스에서의 경험을 고려하지 않았기 때문일 것입니다.




마치며


보안성 낮은 제품을 만드는 사람에게 부족한 것은, 일정이 아니라 보안 관련 능력과 지식, 또는 태도입니다. 접근성도 마찬가지입니다. 보안성 지원이란 말이 어색한 것 처럼, 접근성 지원도 틀린 말입니다.

악성해커와 싸우는 것이 개발자의 숙명이고, 그래서 항시 보안을 고려해 개발해야 하는 것처럼, 다양한 환경에 적응하는 UI를 개발하는 것이 개발자의 숙명이라면, 그는 텍스트를 다뤄야하고, 따라서 접근성 최적화는 개발자의 작업흐름에 자연스럽게 녹아드는 핵심 요소일 수밖에 없습니다.

개발자는 어차피 항상 공부해야 하는 사람들입니다. 아마 이 글을 읽는 독자분들은 이번 주말에도 공부할 주제들이 예정되어 있을 것입니다. 지금까지 일정때문에 접근성 최적화를 미뤄왔다면, 부디 이번 주말에는 각자의 플랫폼에서 접근성 최적화를 어떻게 왜 해야 하는지 공부해보는 것은 어떨까요? 틀림없이, 개발자로서 완전히 새로운 관점을 얻게 되는 계기가 될 것입니다.

특히, 이 분야를 공부하기 위해, 접근성 관련 API 명세들을 들여다보는 일은 분명 지루할 것입니다. 가장 먼저 해야 할 일은, 본인이 스스로 사용자로서 텍스트기반의 인터페이스를 사용해보는 것입니다. 즉, 스크린리더 사용법을 배우고, 눈을 감고 본인의 제품을 사용해보는 것이 이 분야를 가장 재미있게 공부할 수 있는 첫걸음입니다.




참고문서






보다 빠르게 뱅크샐러드에 도달하는 방법 🚀

지원하기

Featured Posts

post preview
post preview
post preview
post preview
post preview

Related Posts