AI 코드 생성 도구는 놀랍도록 빠르게 UI를 완성시켜 준다. 폼을 만들어 달라고 하면 폼이 나오고, 버튼을 요청하면 클릭 핸들러까지 붙어 나온다. 그런데 그 코드가 스크린리더 사용자에게도 제대로 동작하는지, 그리고 클라이언트에서 걸어둔 유효성 검사가 서버에서도 신뢰받을 수 있는지 — 이 두 가지를 AI는 거의 묻지 않는다. 개발 속도가 빨라질수록 이 질문을 던지는 사람이 줄어드는 것이 문제다.
자동화 테스트가 '통과'해도, 사용자는 막힐 수 있다
dev.to에 공개된 라리가 20개 구단 웹 접근성 감사 리포트는 흥미로운 아이러니를 담고 있다. Lighthouse 같은 자동화 도구에서 높은 점수를 받은 사이트가 실제 스크린리더로 탐색했을 때 전혀 다른 모습을 드러낸 것이다. 대표적인 사례가 아라베스(Alavés)다. 자동화 테스트 결과는 양호했다. 모든 이미지에 alt 속성이 붙어 있었으니까. 그런데 수동 테스트를 해보니 그 값이 전부 "null" 이었다. 스크린리더는 충실하게 "link, image, null"을 반복해서 읽어냈다.
이건 단순 실수로 치부하기 어렵다. 코드 생성 파이프라인이든, 템플릿 기반 CMS든, AI 자동완성이든 — 도구가 형식은 맞추지만 의미는 채우지 못하는 패턴이 반복된다. alt 속성의 존재 여부는 검사하지만 그 값이 실제로 유용한지는 기계가 판단하기 어렵다. 오스나(Osasuna)의 캐러셀은 prefers-reduced-motion 설정을 완전히 무시한 채 자동재생됐고, 에스파뇰(Espanyol)의 키보드 내비게이션은 포커스를 가둬버려 탈출이 불가능했다. 이 모든 문제가 자동화 테스트를 통과한 상태에서 발생했다.
반대로 의미 있는 사례도 있다. 아틀레티코 마드리드(Atlético)는 prefers-reduced-motion을 설정하면 애니메이션과 트랜지션이 완전히 멈추는 구현을 갖고 있었는데, 리포트는 이를 "드문 사례"라고 명시했다. 접근성 배려가 얼마나 희귀한지를 역설적으로 드러내는 칭찬이다. Racing de Santander는 자동화 점수는 평범했지만 수동 테스트 최고 점수를 기록했다. 구조가 단순하고, 콘텐츠가 절제돼 있고, 키보드로 실제로 써보기에 무리가 없었기 때문이다.
'접근 가능한 템플릿'을 써도, 콘텐츠가 망가뜨린다
여러 구단이 동일한 화이트라벨 템플릿을 쓰고 있었다. 템플릿 자체는 SEO와 접근성 기반이 잡혀 있었다. 문제는 그 위에 콘텐츠를 얹는 순간 시작됐다. 게타페(Getafe)는 템플릿 기반이었지만 h1이 단 하나도 없었다. 셀타(Celta)는 스페인어 사이트인데 버튼 레이블이 영어로 붙어 있었다 — "복붙의 위험"이라고 리포트는 꼬집었다. 레반테(Levante)는 콘텐츠 자체가 워낙 적어서 접근성 점수가 높았는데, 이건 칭찬이 아니라 채점 기준의 허점이기도 하다.
이 지점이 AI 워크플로우와 연결된다. v0.dev나 Cursor로 컴포넌트를 뽑아낼 때, 도구는 구조를 만들어준다. 하지만 그 구조 안에 들어갈 텍스트, alt, aria-label, 포커스 순서 — 이건 결국 개발자 혹은 콘텐츠 운영자가 채워야 하는 몫이다. 템플릿이 아무리 훌륭해도, 거기에 "null"을 넣는 건 사람의 선택이다.
클라이언트 검증: UX를 위한 것이지, 보안을 위한 것이 아니다
비슷한 구조의 맹점이 보안 레이어에도 존재한다. 클라이언트 사이드 유효성 검사(client-side validation)는 프론트엔드 개발자가 매일 쓰는 도구다. 이메일 형식을 확인하고, 입력 길이를 제한하고, 허용되지 않는 문자를 막는다. 사용자에게 즉각적인 피드백을 주는 훌륭한 UX 장치다. 그런데 이것을 보안 경계로 착각하는 순간 문제가 생긴다.
dev.to의 보안 노트가 핵심을 간단하게 정리한다. 브라우저에서 작동하는 모든 것은 우회될 수 있다. HTML을 직접 수정하거나, JavaScript를 비활성화하거나, curl이나 Burp Suite로 요청을 직접 서버에 보내면 클라이언트 검증은 존재하지 않는 것과 같다. username 필드에 20자 제한을 걸어놓은 폼이 있어도, 공격자는 그 폼을 쓸 이유가 없다.
따라서 서버는 클라이언트가 이미 검증했다고 가정해서는 안 된다. 들어오는 모든 데이터 — 폼 필드, URL 파라미터, JSON 바디, HTTP 헤더, 쿠키, 파일 업로드, API 요청 — 를 신뢰할 수 없는 입력으로 취급해야 한다. "Never trust the client"는 오래된 원칙이지만, AI가 클라이언트 코드를 빠르게 생성해주는 시대에 더 자주 잊힌다.
Allow List와 레이어드 방어: 실용적인 서버 검증 전략
서버 검증에서 특히 효과적인 패턴이 Allow List다. 허용할 값의 목록을 명시적으로 정의하고, 그 밖의 모든 것을 거부한다. 블랙리스트(차단 목록)를 유지하는 것보다 추론하기 쉽고, 예상치 못한 입력에 강하다. 상태값이 active, inactive, pending 세 가지뿐이라면, 그 세 가지만 받으면 된다. 나머지는 에러다.
타입과 범위 검증도 놓치기 쉬운 부분이다. API가 표시할 항목 수를 받는다면, 그 값이 정수인지, 1에서 100 사이인지 서버가 직접 확인해야 한다. 클라이언트에서 limit=100으로 제한해도 limit=999999999를 직접 보내는 건 아무런 기술적 장벽이 없다. 그리고 서버 검증은 다른 보안 레이어를 대체하지 않는다. SQL 인젝션 방어는 파라미터화된 쿼리가, 출력 인코딩은 별도 레이어가 담당해야 한다. 검증은 첫 번째 게이트이지, 전부가 아니다.
AI가 만든 코드, 개발자가 지켜야 할 두 가지 기준
두 주제는 표면적으로 다르지만 같은 구조적 문제를 공유한다. 자동화 도구 — Lighthouse든 AI 코드 생성이든 — 는 형식적 정확성을 빠르게 보장한다. alt가 있는지, 검증 코드가 있는지. 하지만 그 값이 의미 있는지, 그 검증이 올바른 레이어에서 이루어지고 있는지는 여전히 사람이 판단해야 한다.
라리가 구단 감사 리포트가 마지막에 스스로 인정하듯, "AI처럼 나도 실수를 할 수 있다." 이 문장이 흥미로운 건, AI와 자동화 도구가 공유하는 한계를 개발자 본인도 가지고 있음을 솔직하게 드러내기 때문이다. 결국 접근성과 보안 검증은 도구의 결과물을 한 번 더 의심하는 습관 — 그리고 그 습관을 팀의 프로세스로 구조화하는 것에서 출발한다. 속도를 올려주는 도구를 쓸수록, 멈추고 확인하는 사람이 더 필요하다.