React Native 앱 접근성 지원 시작하기

hako kim
newneek
Published in
13 min readJan 31, 2024

--

photo by unsplash

이런 분들에게 도움이 되는 글이에요.

안녕하세요. 뉴닉 프론트엔드 엔지니어 하코입니다. 오늘은 팀에서 제품만드는 과정에서 어떤 접근성을 지원하고 개발했는지 공유하려고 해요. 읽는 동안 React Native에서 접근성 지원이 어렵지 않다는 생각의 계기가 되었으면 좋겠습니다.

이런 분들께 도움이 될 수 있을 거 같아요.

  • 접근성 지원을 시작하고 싶은 React native 개발자
  • Cross-platform 프로덕트에서 어떤 접근성을 지원할 수 있는지 고민하는 기획자
  • 앱 보이스오버(스크린리더) 지원할때 무엇을 고려하는지 궁금한 사람

접근성을 생각하게 된 계기

photo by unsplash

순간순간 문턱

제가 다녔던 부천의 한 고등학교는 수녀회에서 운영하고 있는데요. 학생들의 사회 기여를 중요하게 생각하고 그 필요성을 몸으로 느끼는 교육을 실시합니다. 한번은 눈을 가리고 교정을 도는 프로그램에 참여했습니다. 오전 내내 발에 채는 나무뿌리에 걸려서 넘어질까봐 두려워했던 기억이 납니다.

하나의 사례를 언급했지 이런 렌즈를 삶 전반에 걸쳐 형성했기 때문에 저는 언어, 신체, 성별, 나이 등과 같은 순간순간 문턱을 넘게 해주는 사물과 행위에 자연스레 관심이 많아졌습니다.

정보 접근성은 처음 웹을 개발하면서 관심이 생겼습니다. 몇 줄의 코드로 누군가가 서비스를 이용하는 순간의 문턱을 낮추는데 기여할 수 있다는 점이 멋지다고 생각해요.

디지털 서비스를 세일즈하는 입장의 사회적 책임

Mozilla 한국 커뮤니티의 회원글 , IEEE의 코드 윤리강령

이제 막 이 주제에 대해 알아가기 시작했을때, 위 링킁비슷한 생각을 가진 사람들을 알게 됐어요. 웹과 앱은 대중 누구나 방문하는 디지털이고, 공공적 성격을 띄고 있죠. 한국인 대부분이 생활에 필요한 소식과 정보를 인터넷을 통해 접해요.

그렇다면 이 접근이 누군가에겐 아주 불리한 방식으로 제공되고 있다면?
서비스를 제공하는 사람으로서 접근 장벽을 낮추는 노력을 해야 하는 사회적 책임을 느끼지 않을 수 없었습니다.

지금 팀의 프론트엔드 개발자로 일하면서 접근성 기능을 개발할 수 있는 기회가 많았어요. 오늘은 실제로 했던 작업을 구체적으로 풀어볼게요.

모바일 접근성 이해에 도움이 되는 레퍼런스:

뉴닉에서 작업한 앱 접근성 기능

뉴닉 앱을 개발하면서 일부 접근성 기능이 지원되도록 개발했습니다. 가장 대표적으로 작업한 지원 모드 4가지를 소개합니다.

1. 다크모드

뉴닉앱 다크모드, 라이트모드 캡쳐

‘다크모드 지원’은 앱 출시 초반부터 많은 유저들이 필요하다고 요청한 기능이에요. 디자이너 파트에서 노력해서 선정한 반전컬러를 활용해서 다크모드를 반영했어요. (컬러 시스템은 지금도 꾸준히 발전하고 있답니다)

현재 뉴닉앱은 유저 디바이스상 설정된 테마를 따르도록 구현되어 있는데요. 컴포넌트 파일에서 react-native의 Appearance API를 가져오면 디바이스 상 설정된 테마가 어떤 값인지 알 수 있어요.

import { Appearance } from 'react-native';

export function ThemeProvder(){
const deviceTheme = Appearance.getColorScheme(); // 'light' or 'dark'
const [theme, setTheme] = useState<'light' | 'dark'>(deviceTheme);

const onColorSchemeChange = ({ colorScheme }) =>
setTheme(colorScheme === 'dark' ? 'dark' : 'light');

useEffect(()=>{
const event = Appearance.addChangeListener(onColorSchemeChange);
...
},[]);

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}

앱을 사용하는 중에 테마가 바뀌면 앱 UI를 일괄 변경하기 위해 모드를 Context 전역 상태로 관리하고 있습니다.

2. 보이스오버

보이스오버는 유저가 앱과 인터랙션 할 때 행위가 발생한 인터랙션이 어떤 의미인지 음성으로 설명하는 기능이에요. 이 스크린리더는 iOS에서 보이스오버, AOS는 톡백(talkback)라고 불러요.

뉴닉은 의미가 가장 직관적인 ‘보이스오버’로 부르고 있어요.

아이폰 보이스오버 화면 캡쳐

보이스오버 활성화하면 인터랙션 방식이 달라진다?

그렇습니다. 예를 들어 scroll은 세 손가락으로, touch는 요소 포커스로, press 하려면 focused 상태에서 두 번 눌러야해요. 그리고 인터랙션은 모두 음성으로 변환되고요.

보이스오버를 처음 시작하면 마치 생소한 기계를 쓰는 기분이 들어요. 어색하지만 보이스오버 기능을 제대로 지원하려면 인터랙션 차이를 느끼며 음성을 들어보는 게 도움이 될 겁니다.

Readable 요소로 만들기

스크린리더가 요소를 읽을 수 있으려면 먼저 accessible 해야 합니다. Text 컴포넌트는 기본적으로 accessible 하기 때문에 Text 요소를 누르면 스크린리더가 라벨을 읽어요. View를 읽을 수 있는 요소로 만들기 위해 accessible하게 만들고 라벨을 부여합니다.

<View accessibilityLabel="구매하기" accessibilityRole="button">
<Icon src={image_path} />
<Text>구매하기</Text>
</View>

위 예제에서 role로 “button”이라는 값을 넘겨주고 있어요. 이 값은 API에 지정되어 있는 예약어인데요. 스크린리더가 각 역할에 맞는 이름을 자동으로 읽습니다. (label + role_name) 위 코드는 스크린리더가 ‘구매하기 버튼’ 으로 읽습니다. (https://reactnative.dev/docs/view#accessibilityrole)

3. 가로 모드

뉴닉앱 가로 모드 화면 캡쳐

모바일 기기를 90도 각도로 돌려본 경험이 있나요? 바로 ‘가로 모드’ 입니다. 특히 키보드와 연결하는 갤럭시 탭, 아이패드를 사용하는 유저들이 앱을 사용할 때 필요하다고 느끼는 기능일 거예요.

앱에서 가로모드를 지원하려면 iOS는 Xcode에서 설정이 필요해요. (Info.plist의 UISupportedInterfaceOrientations) 물론 활성화하면 끝이 아니라 이제 시작입니다. UI 대응이 필요하기 때문이죠.

파일에서 @react-native-community/hooks API의 useDeviceOrientation().landscape 를 호출하면 가로모드 상태인지 여부를 반환받을 수 있어요. 실제 프로덕션에서 가로모드가 대응되어야 하는 일부 스타일은 이 라이브러리를 활용해 분기하고 있어요.

export default function Component(): ReactElement {
const orientation = useDeviceOrientation();
console.log(orientation.landscape); // 가로모드일때 true
...
}

가로모드 지원했던 경험을 돌이켜보면…

전달받은 시안을 가로모드로 표현하기 어려운 케이스가 있어요. 디자이너 파트와 소통하고 개발하는 일에 기대 이상으로 리소스를 써야 하는 상황이 종종 발생하고요. 따라서 가로모드는 팀 상황과 전략에 따라 지원 결정, 제공 시점을 숙고해 결정하는 것이 좋습니다.

4. 폰트스케일링

폰트스케일링을 고려한 UI는 뉴닉 제품팀의 디자이너, 개발자가 기본적으로 고려하기 위해 노력하는 지원 중 하나입니다. 시안, 개발 과정에서 이를 고려하지 않으면 폰트 크기가 큰 유저 화면에서 레이아웃이 깨지거나 의도한 유저 경험을 제공하기 어려워요.

서비스 초반에 모든 폰트스케일을 대응하다 보니 ‘여긴 폰트 크기가 큰데…’, ‘늘 반드시 폰트스케일링에 대응하는 것이 좋은 걸까?’라는 생각이 들때가 있었어요.

어느 날 폰트스케일링 대응 후 결과물을 써보니 아래의 문제를 발견했습니다.

  • 화면에 고정된 요소가 커서 scrollable 영역이 좁아지는 문제
  • 상단 헤더의 글자 크기가 과하게 커서 본문 시작이 내려가는 문제

이런 부분은 제품팀 내 동료들에게 얘기하고, 조율을 거쳐 ‘폰트스케일링에 대응하지 않는 케이스’를 적용 할 수 있었습니다.

이 지원을 시작하면서 폰트스케일링이 필요한 이유, 화면 맥락을 해치지 않으면서 대응하는 방법을 생각해 본거 같아요.

지금까지 접근성 지원을 간단하게 소개했는데요. 실은 한 가지 접근성 방식을 지원하더라도 다각도로 고려할 점이 많아요. 스크린리더(보이스오버) 지원을 예시로 들어볼게요.

보이스오버 사용자를 위하여

photo by unsplash

뉴닉 프론트엔드 엔지니어는 앱에서 보이스오버를 이용할 수 있도록 개발하고 있어요. 이 기능을 지원하기 위해 개발팀에서 어떤 점을 고려했는지 자세히 살펴볼게요.

누를 수 있는 요소는 모두 라벨을

뉴닉 제품팀에서 Pressable 요소는 모두 음성지원이 가능한 것을 원칙으로 합니다. child로 Text만 갖는 것이 아니라 아이콘, 복잡한 UI 등을 포함한 Pressable parent는 구체적으로 라벨을 명시하고 있어요.

comboBox처럼 상태가 변하는 Pressable은 state를 넘겨주자

프론트에서는 간혹 comboBox, checkBox처럼 누르면 UI 상태가 변하는 요소들을 다뤄요. 유저가 UI의 현재 상태를 알 수 있어야 기대하는 행동을 수행할 수 있습니다.

그렇다면, 상태는 어떤 방식으로 알릴 수 있을까요? 물론 라벨을 직접 바꾸는 방법도 있지만 accessibilityState 프로퍼티를 사용하면 일관된 언어로 알릴 수 있어요.

const [checked,setChecked] = useState(false);
const onPress = () => setChecked(prev=> !prev)

<Pressable
style={styles.checkbox}
onPress={onPress}
accessibilityRole="checkbox"
accessibilityLabel="이용약관"
accessibilityState={{ checked: checked }}
hitSlop={24}>
<CheckBoxIcon checked={checked} />
</Pressable>

accessibilityState를 쓰려면 React Native API에 구체적으로 정의된 객체를 넘겨야 합니다. (https://reactnative.dev/docs/accessibility#accessibilitystate)

child Text 여러개를 의미있는 단 하나의 단위로

View parent가 여러개의 Text를 child로 갖고있는 상황을 가정해 볼게요.

<View accessible={true}>
<Text style={styles.titleText} accessible={false}>읽음</Text>
<Text style={styles.metaText} accessible={false}>{viewCount}</Text>
</View>

위 UI는 글자와 숫자가 합쳐져야 하나의 유의미한 정보를 표현해요. ‘읽음 {viewCount}’으로 읽어야 하죠. 따라서 한 번의 press로 두 가지 정보를 알려야 하므로 부모는 accessible한 요소로, child Text는 accessible하지 않은 요소로 만들어야 해요.

이모지가 읽기 경험을 해치지 않도록.

라벨에 이모지가 들어가면 시각적으로 주목하게 되지만 보이스오버 유저는 요소의 역할을 헷갈릴 수 있어요. 왜냐하면 보이스오버는 이모지에 해당하는 글자로 읽어주거든요.

// 보이스오버: "돈주머니 구매 버튼"
<Pressable role="button" style={styles.titleText}>
<Text>💰 구매 버튼</Text>
</Pressable>

코드상에서 위 표현의 역할은 가급적 분리되는 게 좋은 방향인 거 같습니다. 문구 표현은 Text에 맡기고 장식효과는 Image, View에게 맡기는 방식처럼요.

시도해보지 않은 방법이고 유저에게 유효할지 모르겠지만요. 이모지를 별도 Text로 분리해 label를 새로 부여하는 parser를 만드는 방법도 상상해봤습니다. 이모지의 사용이 읽기 경험을 해치는지, 충분히 읽을만하게 쓰였는지 판단하는 일이 모호하게 느껴지네요.

마무리 하는 말

어때요? 지원 방법이 생각보다 간단하지 않나요. 보이스오버를 지원하는 경우 React Native는 accessibility 관련 props의 추상화를 사용하기 쉽게 제공하기 때문에 적용이 어렵지 않아요. 지원하는 범위를 점차 늘리는 계획이 있다면 보이스오버 지원을 먼저 경험해 보는 것을 추천합니다.

참고로 Cross-Platform Korea에서 간단히 접근성 작업의 현황 조사를 했는데 “알아갈 의사가 있다"고 표시해주신 분들이 계셨어요.

커뮤니티 소통채널에서 수요 조사하는 모습

지금 시작해야 하는 이유

개인적으로 작업하면서 가장 어려웠던 점은 우선순위 균형을 맞추는 일이었어요. 이 작업이 중요하다는 것을 알면서도 프로덕트를 빠르게 성장시켜야 하는 전력에서는 타협하려고 했던 순간들이 있었던거 같아요.

그치만 매일 코드를 만드는 일상에서 그 책임을 다하지 못할 때, 어려움이 있을 때는 이상과 현실의 격차에 괴로워하기보다 실행 에너지를 더 쓰기로 했습니다. 덜 타협하는 방법, 바쁜 상황에서 개발하면서 더 쉽게 지원하는 방법처럼요.

주변 개발자와 얘기하다보면 서비스 접근성을 본격 지원하는 일은 현실적으로 조직의 의지, 문화가 뒷받침 되어야 하는 영역이라는 생각이 듭니다.

뉴닉의 제품팀은 PM, 디자이너, 개발자 모두 ‘우리는 접근성이 중요하다’는 공감대가 있는 사람들이 모여있어요. 운 좋게도 뉴닉과 가치관이 맞았기에 초기 프로덕트부터 개발과정에서 지원 기능을 신경쓸 수 있었습니다.

완성 , 완벽을 생각한다면 시작하는 것조차 부담이 크죠. 시작이 미약해도 제품을 만드는 팀 안에서 ‘지금 시작해야 하는 이유'를 공감하고 계속하는 것이 제일 중요하다고 생각합니다.

진짜 마지막

멈추지 않고 묵묵히 해나가는 뉴닉 제품팀, 그리고 온라인 제품을 만드는 업계 동료들에게 응원과 지지를 보냅니다.

뉴닉처럼 서비스의 접근성에 꾸준히 기여하고 있는 팀이 있다면 댓글로 알려주세요. 그리고 접근성 지원과 관련해 도움이 필요하다면 댓글이나 khk0503@icloud.com 로 메일을 보내주세요.

읽어주셔서 감사합니다!

--

--