| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
ScreenReaderStudio는 웹 페이지나 HTML 코드를 입력하면, 스크린 리더가 어떻게 읽을지를 시각적으로 예측하고 접근성 문제를 진단해 주는 도구입니다. 스크린 리더 환경을 직접 확인하기 어려운 개발자를 위해, 웹 접근성 개선을 직관적이고 효율적으로 지원합니다.
Deployed website | Frontend Repository | Backend Repository
프론트엔드는 Next.js App Router를 기반으로 하며, 라우팅과 업무 로직의 책임을 분리하기 위해 도메인 모듈 중심으로 구성했습니다.
src/
├── app/ # 라우트, 레이아웃, 페이지 조합
├── components/
│ ├── layout/ # 여러 페이지에서 사용하는 레이아웃 컴포넌트
│ └── ui/ # 도메인에 의존하지 않는 공통 UI
├── modules/
│ ├── analysis/
│ │ ├── api/ # 분석 실행, 저장, 조회 API
│ │ ├── components/ # 입력 및 결과 화면
│ │ ├── model/ # Zustand 상태
│ │ ├── constants.ts
│ │ └── types.ts
│ └── auth/
│ ├── api/ # 사용자 조회, 로그인, 로그아웃 API
│ ├── components/ # 인증 UI
│ ├── AuthProvider.tsx
│ └── types.ts
├── providers/ # 애플리케이션 전역 Provider
├── shared/
│ ├── api/ # 공통 API 클라이언트
│ ├── config/ # 공통 설정과 상수
│ ├── lib/ # 범용 유틸리티
│ └── types/ # 공통 타입
└── test/ # 전역 테스트 설정
public/ # 이미지와 폰트 등 정적 리소스
토스의 기술 블로그 게시글 중 우리가 몰랐던 시각 장애인의 UX을 읽다가 다음과 같은 문장을 접했습니다.
“그런데 스크린 리더가 ‘아이콘 하트’라고 읽어주면, 사용자는 이게 찜하기 버튼인지 알기 어려워요.”
이 문장을 통해, 시각장애 사용자에게는 스크린 리더가 읽어주는 한 문장, 한 단어가 곧 UI의 전부일 수 있다는 사실을 다시금 인식하게 되었습니다. 시각 정보 없이 콘텐츠를 해석해야 하는 환경에서, 텍스트 외 요소들이 어떻게 설명되느냐는 곧 UX의 핵심이 될 수 있기 때문입니다.
또한, 이 문구를 읽는 순간, 과거 웹 개발 중 img 태그에 alt 속성을 빠뜨렸을 때 콘솔에서 경고 메시지를 본 기억이 떠올랐습니다. 그 당시에는 단순히 린트 에러처럼 여겼던 그 경고가, 이제는 사용자에게 ‘이 이미지는 무엇인가’를 설명해주는 기능이었음을 깨닫게 된 것 이었습니다.
이와 같은 경험을 통해, 우리는 잘 만들었다고 생각하는 웹 서비스에서도 어떤 사용자에게는 사용하기 어려운 서비스일 수 있다는 점을 체감하게 되었습니다. 하지만 스크린 리더의 동작을 실제로 확인해보는 것은 여전히 어렵고, 개발 도중 혹은 배포 이후에도 즉각적으로 접근하기 힘든 영역이기도 합니다.
따라서, 스크린 리더가 실제로 어떻게 읽어줄지를 시각적으로 예측해주고, 접근성 문제를 자동으로 진단해주는 도구가 있다면 분명 도움이 될 것이라는 문제의식이 생겼습니다.
이러한 계기로, 개발자와 디자이너가 스크린 리더 환경을 별도의 설정 없이 손쉽게 파악하고, 접근성 이슈를 빠르게 인지하고 개선할 수 있도록 돕는 도구인 ScreenReaderStudio를 개발하게 되었습니다.
서비스의 첫 화면입니다. 진행하고 싶은 페이지의 URL 혹은 HTML 코드를 입력한 후 원하는 스크린 리더 타입을 선택한 뒤, 분석 버튼을 눌러 스크린 리더 대본 생성 및 접근성 이슈 분석을 진행할 수 있습니다.
선택한 스크린 리더 타입에 해당하는 스크린 리더 대본이 생성됩니다. 원하는 항목을 선택하면 해당 항목을 하이라이팅으로 확인할 수 있고, TTS로 음성이 출력됩니다.
axe-core를 통한 접근성 이슈 분석의 결과 화면입니다. 영향 요소를 선택하면 해당 요소가 iframe에서 하이라이팅되어 어떤 요소에 접근성 이슈가 있는지 확인할 수 있습니다.
로그인한 사용자에 한하여 분석 결과를 공유할 수 있습니다. 카카오 로그인을 통해 회원가입 및 로그인을 진행할 수 있습니다.
위와 같이 사용자가 로그인 한 경우, 분석 결과를 공유할 수 있는 링크 복사 버튼이 생깁니다. 해당 버튼을 클릭하면, 공유 링크가 복사되고 결과를 다른 사람과 공유할 수 있습니다.
공유 링크를 방문하면 위와 같이 분석을 진행한 사용자가 아니더라도 스크린 리더 대본과 접근성 이슈 분석 결과를 확인할 수 있습니다.
초기 기획 단계에서 ScreenReaderStudio는 사용자가 입력한 페이지 URL이 아닌, 개별 컴포넌트의 JSX 코드만을 기반으로 접근성 분석 및 스크린 리더 대본을 생성하는 구조를 고민했습니다. 이는 개발자 친화적인 인터페이스를 제공하며, Storybook이나 디자인 시스템 같은 문맥에서 개별 컴포넌트 단위로 접근성을 확인할 수 있도록 하기 위함이었습니다.
이를 위해 Babel을 사용해 JSX 코드를 JavaScript AST로 변환하고, @babel/traverse로 트리를 순회하며 접근성 이슈를 탐지하는 분석 로직을 구성하려 했습니다.
초기에는 개발자 관점에서 접근하기 쉬운 인터페이스를 제공하고자 JSX 코드 기반의 분석을 시도했지만, 점차 접근성 분석의 본질은 정적 코드 분석이 아닌 실제 사용자 환경에서의 인터랙션 가능성, 화면 구조, 콘텐츠 흐름을 바탕으로 해야 한다는 사실을 직시하게 되었습니다.
그 결과, "코드가 아닌 웹페이지 전체를 대상으로 분석해야 한다"는 관점을 받아들이게 되었고, “URL 기반 접근성 분석” 이라는 방향으로 전환하게 되었습니다.
접근성 분석의 정확성과 실효성을 보장하기 위해, JSX 코드 대신 전체 웹페이지를 Headless Browser로 렌더링하고 분석하는 구조로 변경했습니다. (→ 2. 클라이언트에서는 iframe을 사용하려면 겪는 문제와 해결 방법 참고)
JSX 코드 기반의 정적 분석은 기술적으로 시도해볼 수 있는 접근이었지만, 실제 웹 접근성 분석이라는 목적에는 부합하지 않는 방법이었습니다. 결과적으로 우리는 “코드를 분석하는 것이 아니라, 사용자에게 보여지는 결과를 분석하는 것이 본질”이라는 점을 깨달았습니다.
이러한 반성과 판단을 통해, ScreenReaderStudio는 코드 기반 분석이 아닌 URL 기반 페이지 분석 중심의 아키텍처로 재설계되었고, 이는 이후 iframe, 서버 렌더링, Skeleton UI 도입 등 모든 기술 결정의 기반이 되었습니다. 이 과정은 단순한 기술 변경이 아닌, 문제의 본질을 다시 정의하고 구조적으로 대응한 전환의 시작점이었습니다.
ScreenReaderStudio는 초기에는 사용자가 입력한 웹페이지 URL을 클라이언트 측 iframe에 로드하여, 접근성 분석 및 스크린 리더 대본을 생성하려는 방향을 검토했습니다. 이는 서버 인프라를 줄이고, 빠른 분석 피드백을 제공하기 위한 가볍고 직관적인 아키텍처를 지향한 선택이었습니다.
iframe을 통해 실제 페이지를 불러오고, DOM을 분석하거나 음성 대본을 생성하는 방식을 고려했으나, 보안 정책과 브라우저 동작 원리에 의해 치명적인 한계를 갖고 있다는 사실을 점차 깨닫게 되었습니다.
Access to XMLHttpRequest at 'https://ko.wikipedia.org/...' from origin 'http://localhost:3000' has been blocked by CORS policy초기에는 iframe을 통해 웹페이지를 직접 브라우저에 불러오고, 그 상태에서 접근성 분석을 수행하는 것이 가장 빠르고 직관적인 방법이라고 판단했습니다. 그러나 점차 분석이 실패하는 사례들이 누적되면서, CORS, sandbox, 자원 로딩 등 다양한 보안 정책과 브라우저의 기본 동작 원리에 의해 클라이언트 측에서 외부 페이지를 안전하게 다루는 것이 구조적으로 불가능하다는 사실을 인식하게 되었습니다.
“그렇다면 어떻게 해서 브라우저는 우리가 iframe으로 불러온 외부 페이지를 이렇게 강하게 보호할 수 있는가?”라는 의문에서 출발해, 우리는 웹의 보안 모델을 더 깊이 조사하기 시작했습니다. 이 과정에서 브라우저는 사용자의 보안을 위해 외부 콘텐츠에 대한 자바스크립트 접근을 철저히 제한하도록 설계되어 있으며, 특히 클라이언트 환경에서는 보안 위협(CSRF, XSS 등)을 막기 위해 의도적으로 많은 기능이 봉인되어 있다는 사실을 이해하게 되었습니다.
하지만 조사 도중 발견한 흥미로운 사실은, 서버에서는 이런 제약이 존재하지 않는다는 점이었습니다. 서버에서는 브라우저처럼 보안 상의 이유로 요청을 제한하지 않으며, Puppeteer와 같은 Headless Browser를 통해 실제 브라우저 환경을 에뮬레이션하여 페이지를 로드하고 분석할 수 있습니다. 서버는 동일 출처 정책이나 CORS를 적용받지 않기 때문에, 클라이언트에서 차단되던 외부 요청이나 DOM 접근도 자유롭게 가능합니다.
결과적으로 우리는 “웹페이지를 클라이언트가 아닌 서버에서 렌더링하고 분석해야 한다”는 방향으로 전환하게 되었고, 이는 단순한 기술 스택 변경이 아니라, 웹의 보안 모델을 이해한 끝에 내린 구조적 전환이었습니다.
iframe 기반 접근의 한계를 극복하기 위해, 서버에서 Headless Chrome(Puppeteer)을 통해 페이지를 실제로 렌더링하고, axe-core를 통해 DOM을 분석하는 구조로 전환했습니다.
iframe을 활용한 클라이언트 기반 분석은, 초기에는 가장 단순하고 효과적인 방법처럼 보였으나, 보안 정책(CORS/sandbox)과 브라우저 환경 제약으로 인해 구조적으로 실현 불가능한 방법이었습니다.
우리는 “브라우저는 외부 페이지를 보호하려는 방향으로 설계되어 있다”는 웹 보안의 본질을 마주하게 되었고, 분석을 클라이언트가 아닌 서버에서 수행해야 한다는 명확한 판단에 도달하게 되었습니다.
결국 ScreenReaderStudio는 iframe 기반 분석이 아닌, 서버 측 Headless Browser + 접근성 분석 엔진(axe-core) 조합을 중심으로 구조를 재설계했으며, 이는 단순한 기술 스택 변경을 넘어 웹 보안 모델에 대한 이해를 바탕으로 한 구조적 전환의 결과였습니다.
ScreenReaderStudio는 사용자가 입력한 웹페이지를 서버 측에서 Headless Browser로 렌더링하고, DOM을 기반으로 axe-core를 통해 접근성 분석 및 스크린 리더 대본을 생성하는 구조입니다. 이러한 분석 과정은 웹페이지의 복잡도와 네트워크 상태에 따라 수 초 이상의 시간이 소요될 수 있습니다. 이는 정적인 분석 툴과는 달리 브라우저 수준의 렌더링을 포함하기 때문입니다.
초기에는 결과 화면에 접근성 이슈와 스크립트가 모두 준비된 이후에만 결과 UI를 보여주는 구조였습니다. 하지만 분석 로직이 실행되고 있다는 피드백이 사용자에게 전혀 전달되지 않는다는 치명적인 UX 문제가 존재했습니다. 사용자 입장에서는 “버튼을 눌렀는데 아무런 반응이 없다”는 인상을 받을 수 있으며, 이로 인해 페이지를 이탈하거나 다시 요청을 보내는 불필요한 행위가 발생할 수 있습니다.
단순한 텍스트 로딩 메시지나 spinner 아이콘 하나로는 해결이 어렵다고 판단했습니다. 이 문제는 단순히 "로딩이 보이지 않는다"는 UI 문제를 넘어서, 사용자에게 "서비스가 현재 제대로 작동 중이다" 라는 심리적 확신을 줘야 하는 문제였기 때문입니다.
특히 ScreenReaderStudio의 경우, 분석 결과가 나타나기 전까지는 화면에 아무런 콘텐츠가 없기 때문에, 분석 결과의 형태를 예상 가능하게 시각적으로 제공해줄 필요가 있었습니다.
위 문제를 해결하기 위해, 결과 화면의 최종 분석 결과 레이아웃을 미리 흉내 내는 Skeleton UI를 도입했습니다.
Skeleton UI는 분석이 진행되는 동안에도 사용자에게 화면 구성의 윤곽을 먼저 보여줌으로써, 결과가 곧 제공될 것이라는 기대감을 유지시켜주는 전략적인 UI 패턴입니다.
로딩이 길어질 수밖에 없는 구조적 특성을 가진 서비스에서는, 단순한 스피너나 텍스트로는 부족합니다. 사용자에게 분석이 진행되고 있다는 명확한 피드백을 제공하고, 기대감을 유지시키기 위한 시각적 구조가 필요합니다.
이러한 UX 문제를 해결하기 위해 Skeleton UI를 도입함으로써, 분석 과정의 신뢰도와 사용자 경험을 크게 향상시킬 수 있었습니다. 이 결정은 단순한 디자인 변경이 아닌, 사용자 이탈을 막고 서비스 품질을 높이기 위한 전략적 UI 최적화였습니다.
기존 프론트엔드는 POST /api/analysis/perform 요청 하나를 보내고 최대 120초 동안 결과를 기다렸습니다. 이 방식에서는 HTTP 연결이 분석 작업 그 자체였기 때문에 탭을 새로고침하거나 네트워크가 잠시 끊기면 진행 중인 작업을 다시 찾을 방법이 없었습니다. 서버가 분석을 계속하고 있어도 화면에는 오류만 남을 수 있었고, 사용자가 다시 분석 버튼을 누르면 고비용 Puppeteer 작업이 중복 생성될 가능성도 있었습니다.
Skeleton UI는 기다리는 동안의 시각적 불안은 줄여주었지만, 실제 진행 단계, 취소 상태, 새로고침 복구, 안전한 재시도까지 해결하지는 못했습니다. 서버가 분석을 비동기 작업으로 전환함에 따라 프론트엔드도 단순한 요청·응답 코드가 아니라, 오래 살아 있는 작업의 상태를 일관되게 관리하도록 다시 설계했습니다.
실시간 전달 방식으로 SSE나 WebSocket도 고려할 수 있었지만, 첫 구현은 서버가 안내한 간격으로 상태를 조회하는 polling을 선택했습니다. 현재 배포 구조에서 프록시와 장시간 연결을 추가로 관리하지 않아도 되고, 새로고침 후 동일한 조회 API를 호출하는 것만으로 복구할 수 있기 때문입니다.
프론트엔드의 전체 흐름은 다음과 같습니다.
분석 버튼
│
├─ 접근 토큰 + 멱등성 키 생성
▼
POST /api/analysis/jobs ──▶ 202 { jobId, pollAfterMs }
│
├─ sessionStorage에 복구 정보 저장
├─ GET /api/analysis/jobs/:jobId 반복 조회
│ queued → running → succeeded / failed / cancelled
│
├─ succeeded: GET /result → 기존 ResultViewer에 연결
├─ failed: 오류별 복구 안내와 재시도
└─ cancelled: 서버 확정 후 로딩 종료
작업의 네트워크 생명주기와 React 컴포넌트의 렌더링 생명주기를 분리하고, Zustand store가 생성부터 완료까지 한 작업의 소유권을 갖도록 했습니다.
Web Crypto API의 crypto.getRandomValues로 각각 32바이트, 즉 256비트 난수를 만들어 Base64URL 문자열로 변환했습니다.
작업 생성 요청이 네트워크 오류로 실패하면 같은 입력과 두 비밀값을 유지한 채 지수 backoff로 최대 3회 재시도합니다. 반면 오류 화면에서 사용자가 명시적으로 다시 시도하면 새 비밀값을 생성하므로 이전 작업을 재사용하지 않고 새로운 분석을 시작합니다.
useAnalysisStore에는 기존 결과 데이터와 함께 아래 상태를 추가했습니다.
작업마다 별도의 AbortController를 만들고 현재 controller와 일치하는 응답만 store에 반영합니다. 새 분석이나 공유 결과 표시가 시작되면 이전 controller를 중단하며, 늦게 도착한 이전 polling 응답이 최신 화면을 덮지 못하게 했습니다.
작업 생성 응답의 pollAfterMs와 이후 상태 응답의 값을 다음 조회 간격으로 사용합니다. 현재 서버 기본값은 2초입니다. polling 중 일시적인 네트워크 TypeError가 발생하면 간격을 지수적으로 늘리며 최대 3회 재시도하고, 서버가 명시적으로 반환한 API 오류는 즉시 사용자용 오류 상태로 변환합니다.
상태별 동작은 명확히 분리했습니다.
| 서버 상태 | 프론트엔드 동작 |
|---|---|
| queued, running | 단계와 경과 시간을 갱신하며 계속 polling |
| succeeded | 결과 API를 한 번 호출하고 기존 결과 화면에 연결 |
| failed | 서버 오류 코드에 맞는 원인과 복구 방법 표시 |
| cancelled | polling과 타이머를 종료하고 취소 상태 표시 |
작업이 생성되면 jobId, 접근 토큰, 생성 시각, 선택한 스크린 리더만 sessionStorage에 저장합니다. 원본 URL이나 HTML은 저장하지 않습니다.
AccessibilityAnalyzer가 마운트될 때 resumeAnalysis()를 호출해 저장된 작업을 즉시 조회하고, 아직 진행 중이면 같은 polling 흐름을 다시 시작합니다. 성공, 실패, 취소가 확정되면 저장 정보를 삭제합니다. 따라서 진행 중인 작업은 같은 탭에서 새로고침해도 복구되지만, 완료 결과를 새로고침 후 영구 복원하는 용도는 아닙니다.
기존 Skeleton 레이아웃은 유지하면서 서버 단계마다 안내 문구를 연결했습니다.
| 단계 | 사용자에게 표시하는 의미 |
|---|---|
| queued | 분석 순서를 기다리는 중 |
| launching_browser | 분석 브라우저를 준비하는 중 |
| loading_page | 분석할 페이지를 불러오는 중 |
| analyzing_accessibility | 접근성 문제와 예상 대본을 분석하는 중 |
| preparing_result | 분석 결과를 정리하는 중 |
화면에는 문구와 경과 초를 표시하고, 로딩 컨테이너에는 aria-busy="true"와 aria-live="polite"를 적용했습니다. 스크린 리더 전용 텍스트에도 같은 단계와 경과 시간을 제공해 시각적 Skeleton만으로 정보를 전달하지 않도록 했습니다.
사용자가 취소 버튼을 누르면 DELETE /api/analysis/jobs/:jobId를 호출하고 버튼을 취소 중... 상태로 비활성화합니다. 실행 중 작업은 서버 worker가 취소 신호를 확인할 때까지 짧은 시간이 걸릴 수 있으므로, 프론트엔드는 즉시 성공으로 간주하지 않고 polling을 계속해 cancelled가 확정된 뒤 종료합니다.
기존 분석 오류에 아래 비동기 작업 오류를 추가했습니다.
오류 코드마다 사용자용 메시지, 복구 방법, 재시도 가능 여부를 함께 정의해 HTTP 상태 코드가 그대로 UI에 노출되지 않게 했습니다.
분석이 성공한 로컬 작업이고 로그인한 경우에만 공유 버튼을 표시합니다. 공유 시 { jobId }와 X-Analysis-Job-Token만 서버에 보내며, 접근성 결과·페이지 콘텐츠·대본은 다시 전송하지 않습니다. 서버가 저장된 완료 결과를 검증해 공유 데이터를 만들기 때문에 payload가 작아지고 클라이언트의 결과 변조 가능성도 줄었습니다.
프론트엔드의 비동기 전환은 API 경로를 바꾸는 작업에 그치지 않았습니다. 생성 요청의 멱등성, 장시간 polling, 경쟁 상태 방지, 새로고침 복구, 취소 확인, 오류 복구, 접근성 있는 진행 안내를 하나의 사용자 흐름으로 연결해야 했습니다.
그 결과 서버 작업은 브라우저 연결과 독립적으로 실행되고, 사용자는 그 작업의 현재 상태를 이해하며 필요할 때 취소하거나 복구할 수 있게 되었습니다. 또한 기존 결과 화면과 공유 기능을 유지하면서 비동기 프로토콜을 도입해, 앞으로 SSE나 전용 작업 화면으로 확장할 수 있는 기반을 마련했습니다.
이번 프로젝트를 진행하며 혼자서 서비스의 처음부터 끝까지 책임지는 일이 얼마나 큰 도전인지를 깊이 느꼈습니다. 어떤 컴포넌트가 필요할지부터 시작해서 화면 구성을 어떻게 할지, 또 어떻게 사용자에게 전달할지까지 전 과정을 스스로 판단하고 선택해야 했습니다. 그동안 당연하게 여겼던 의사결정조차 스스로 내리려니 더 많은 고민이 필요했고, 작은 선택 하나에도 결과가 달라질 수 있다는 점에서 많은 부담을 느끼기도 했습니다. 하지만 동시에, 내가 직접 모든 흐름을 설계하고 구현해냈다는 점에서 큰 성취감과 자신감을 얻을 수 있었습니다.
프로젝트 중에는 기술적인 도전도 많았습니다. 그동안 문서로만 접했던 도구나 라이브러리를 실전에서 사용해보며, 단순히 사용법을 익히는 것을 넘어 ‘이 도구가 어떤 문제를 해결하기 위한 것인가’를 이해하려고 노력했습니다. 특히 처음으로 Next.js를 제대로 사용해 보면서, 프레임워크에 대한 표면적인 이해를 넘어, SEO, App Router, SSR 등의 개념을 직접 적용하고 경험할 수 있었습니다. 기술을 배우는 것 자체보다 중요한 건 ‘문제를 해결하기 위한 적절한 수단으로 기술을 고르는 능력’이라는 점을 체감한 순간이기도 했습니다.
개발을 진행하면서 사용자 경험에 대해 더 깊이 고민하게 된 것도 중요한 변화였습니다. 시각장애인이 웹을 사용하는 방식을 시뮬레이션하는 과정에서, 우리가 너무 쉽게 간과하고 있던 요소들이 실제 사용자에게는 얼마나 큰 장벽이 되는지를 실감할 수 있었습니다. 단순히 작동하는 UI를 만드는 것을 넘어서, 누구에게나 접근 가능한 UI를 만들기 위한 기준과 태도가 필요하다는 것을 배웠습니다. 보이지 않는 정보를 어떻게 전달할 것인지, 화면에 보이는 요소들을 어떻게 더 명확하게 설명할 수 있을지를 고민하면서, 접근성은 선택이 아니라 기본이 되어야 함을 깨달았습니다.
이번 프로젝트는 단순히 기술적인 성장에 그치지 않고, 개발자로서 가져야 할 문제 해결 태도, 사용자 중심의 시각, 그리고 더 나은 서비스를 만들기 위한 책임감을 다지는 계기가 되었습니다. 끝으로, 내가 만들고 있는 서비스가 단지 기능적으로 완성되는 것을 넘어서, 누군가에게 실질적인 도움이 될 수 있다는 믿음이 생긴 것이 가장 큰 수확이었습니다.
| Back | FazBrowse Home | New Git URL |