얼마 전 프로젝트를 마치고 QA(품질 검사) 단계를 진행하던 중, 지인에게 피드백을 받았습니다. "마우스 없이 Tab 키로만 메인 페이지 조작해봤어? 모달 창이 열렸는데 초점이 모달 뒤로 넘어가서 닫을 수가 없네."

순간 아차 싶었습니다. 그동안 저는 단순히 예쁜 화면을 만드는 데만 집착했지, 마우스를 쓰기 힘든 사용자나 스크린 리더를 쓰는 사용자가 내 사이트를 어떻게 이용할지 단 한 번도 깊게 고민해 본 적이 없었던 것입니다.

웹 접근성(A11y)은 배려나 선택이 아닌, 모든 사용자에게 정보를 평등하게 전달하기 위한 기본입니다. 오늘은 제가 프로젝트에서 겪었던 시행착오와 함께, 프론트엔드 개발자가 반드시 챙겨야 할 웹 접근성 실무 적용기를 정리해 보려고 합니다.

1. 시맨틱 마크업: <div> 습관을 버리는 것부터 시작이었다

초보 시절 저의 HTML 코드는 그야말로 <div>와 <span>의 파티였습니다. CSS로 모양만 맞추면 장땡이라고 생각했거든요. 하지만 스크린 리더는 이 <div>들을 그냥 아무 의미 없는 박스로 인식합니다.

 
<!-- ❌ 내가 과거에 짰던 코드 -->
<div className="header">
  <div className="logo">My Logo</div>
  <div className="menu">...</div>
</div>

이걸 깨닫고 난 뒤, 프로젝트 리팩토링을 진행하며 HTML5 시맨틱 태그로 전부 교체했습니다.

  • <header>: 페이지나 섹션의 머리말
  • <nav>: 네비게이션 링크 모음
  • <main>: 문서의 핵심 콘텐츠 (페이지당 한 번만 사용)
  • <article>: 독립적으로 배포 가능한 콘텐츠
  • <footer>: 페이지 하단 정보

태그 하나만 고쳤을 뿐인데, 구글 Lighthouse로 검사해 보니 접근성 점수가 급상승하는 것을 보고 시맨틱 마크업의 위력을 실감했습니다.

2. WAI-ARIA: "아이콘 버튼에 이름을 주세요"

제가 가장 많이 실수했던 부분이 바로 '아이콘 버튼'이었습니다. 돋보기 모양 아이콘이 달린 검색 버튼, 'X' 표시고 되어있는 닫기 버튼을 만들 때, 저는 텍스트 없이 <button><IconX/></button> 형태로만 코드를 짰습니다.

일반 사용자는 'X' 그림을 보고 닫기 버튼인 것을 알지만, 시각 장애인용 스크린 리더는 이 버튼을 만나면 그저 "버튼"이라고만 읽어줍니다. 무슨 버튼인지 알 길이 없는 것이죠.

💡 실무 적용법 (aria-label)

 
<!-- ⭕ 이렇게 바꾸고 나서야 스크린 리더가 "닫기, 버튼"이라고 읽어주기 시작했습니다 -->
<button aria-label="모달 닫기" onClick={closeModal}>
  <CloseIcon />
</button>

코드설명

3. 키보드 접근성: 나를 가장 당황하게 만든 '포커스 트랩'

피드백에서 지적받았던 모달 창 이슈는 바로 '키보드 트랩(Keyboard Trap)' 문제였습니다.

레이어 모달을 띄웠을 때, 키보드의 Tab 키를 계속 누르면 초점이 모달 내부의 [확인], [취소] 버튼에서만 순환해야 합니다. 그런데 제 코드에서는 초점이 모달 뒤쪽에 깔려있는 메인 페이지의 메뉴들로 빠져나가는 현상이 발생했던 것입니다.

CSS
 
/* ❌ 디자인 깔끔하게 한다고 절대 쓰지 말아야 할 CSS 스타일! */
button:focus {
  outline: none; /* 이 코드를 넣는 순간, 키보드 사용자는 내가 어디를 가리키고 있는지 전혀 볼 수 없게 됩니다. */
}

저는 예쁘지 않다는 이유만으로 outline: none을 습관적으로 넣었었는데, 이 퍼런 포커스 테두리가 키보드 사용자에게는 '마우스 커서'와 같은 역할을 한다는 것을 깨닫고 즉시 삭제했습니다. 대신 브라우저 기본 포커스링을 살리거나, 커스텀 스타일(:focus-visible)로 깔끔하게 포커스 영역을 표시하도록 고쳤습니다.

4. 직접 경험해 본 접근성 테스트 방법

이론을 아는 것보다 중요한 건 "내가 직접 짠 사이트를 마우스 없이 써보는 것"이었습니다. 이번 일을 계기로 저는 배포 전 반드시 아래 3가지 과정을 거치고 있습니다.

구글 lighthouse

  1. 마우스 치우기: 오직 Tab, Shift + Tab, Enter 키로만 내 사이트의 모든 메뉴와 폼(Form)을 조작해 봅니다.
  2. Mac VoiceOver 실행: Mac 사용자의 경우 Command + F5를 누르면 스크린 리더가 켜집니다. 눈을 감고 내 사이트의 텍스트가 자연스럽게 읽히는지 들어봅니다. (처음 들었을 때 내가 짠 코드가 얼마나 엉망으로 읽히는지 알고 충격을 받았습니다.)
  3. Lighthouse 검사: 크롬 개발자 도구에서 접근성 점수를 측정하고 레포트에서 지적해 주는 요소(alt 누락, 색상 대비 부족 등)를 하나씩 해결합니다.

5. 글을 마치며: 개발자로서의 시야가 넓어진 계기

예전의 저에게 웹 접근성은 "나중에 시간 남을 때 챙기는 부수적인 작업"이었습니다.하지만 이번에 직접 코드를 고쳐보고 테스트해 보면서, 접근성은 개발 초반 뼈대를 세울 때부터 당연히 고려해야 하는 '기본적인 개발 품질'이라는 것을 배웠습니다.

버스를 탈 때 경사로가 설치되어 있으면 휠체어 이용자뿐만 아니라 유모차를 끄는 부모님, 다리를 다친 사람 모두가 편리해집니다. 웹도 마찬가지입니다. 접근성을 고려해 만든 깔끔한 시맨틱 구조와 키보드 지원은 결국 일반 사용자의 편의성과 검색 엔진 최적화(SEO)로까지 이어집니다.

얼마 전 프론트엔드 최신 에디터로 떠오른 Cursor를 도입하고, 프로젝트의 유틸리티 로직 작성을 AI에게 맡겨본 적이 있습니다. 간단한 데이터 변환 함수를 몇 초 만에 뚝딱 만들어내는 모습을 보며 감탄하기도 했지만, 동시에 오한이 서리기도 했습니다. "단순히 Figma 시안을 보고 HTML/CSS 레이아웃을 잡는 일이라면, 머지않아 AI가 전부 대체하겠구나."

실제로 최근 프론트엔드 생태계는 단순히 '화면을 구현하는 작업'에서 'AI와 협업하여 아키텍처를 설계하고, 브라우저 환경에서 고성능 로직을 다루는 영역'으로 급격하게 이동하고 있습니다. 오늘은 제가 실무에서 AI 툴과 WebAssembly(Wasm) 관련 기술들을 직접 접해보고 느낀 솔직한 생각과 준비 전략을 적어보려고 합니다.

1. 코더(Coder)에서 설계자(Architect)로: AI 도구를 써보며 깨달은 점

GitHub Copilot이나 Cursor, ChatGPT 등을 실제 개발 흐름에 녹여내면서 가장 크게 느낀 것은 "개발자의 핵심 역량이 완전히 달라졌다"는 사실입니다.

💡 실무에서 겪은 AI의 한계와 경험

한번은 복잡한 상태 관리 로직을 AI에게 코딩해 달라고 요청한 적이 있습니다. 그럴듯한 코드를 내놓았지만, 실제 프로젝트에 적용해 보니 리액트의 불변성 규칙을 깨뜨려 재렌더링 버그(Hallucination)를 유발하더군요.

이때 깨달았습니다. AI 시대에 중요한 것은 문법을 외우는 것이 아니라, 다음과 같은 능력이라는 것을요.

  • 컨텍스트 전달 능력: AI에게 전체 아키텍처 맥락을 정확히 전달하고 요구사항을 명확히 정의하는 능력
  • 디버깅 및 검증 능력: AI가 짠 '그럴듯하지만 버그가 있는 코드'를 잡아낼 수 있는 깊이 있는 CS(Computer Science) 지식
  • 추상화 설계: 중복되는 로직을 깔끔하게 구조화하여 AI가 오작동하지 않도록 틀을 짜주는 능력

cursor에서 ai로 코드리뷰하기

2. WebAssembly(Wasm): 브라우저의 한계를 넘어서는 경험

자바스크립트(JavaScript)는 참 매력적인 언어지만, 대용량 데이터 처리나 이미지/영상 편집, 3D 연산 같은 고성능 작업에서는 한계가 분명합니다. 이 한계를 깨부수기 위해 등장한 것이 바로 WebAssembly(Wasm)입니다.

🚀 왜 Wasm에 주목해야 할까?

Figma가 웹 브라우저에서 데스크톱 프로그램보다 부드럽게 돌아가는 이유, Photoshop이 웹 버전으로 출시될 수 있었던 이유가 바로 C++이나 Rust로 짜인 핵심 로직을 Wasm으로 빌드하여 브라우저에서 직접 돌리기 때문입니다.

요즘 저도 Rust 기초를 조금씩 공부하기 시작했는데, 자바스크립트로 처리할 때 버벅이던 대용량 배열 연산을 Rust + Wasm 조합으로 변환해 실행해 보니 속도 차이가 체감될 정도로 빨라지는 것을 경험했습니다. 앞으로는 UI는 React로 짜고, 성능이 중요한 연산 로직은 Rust/Wasm으로 구현하는 '하이브리드 개발'이 프론트엔드의 강력한 무기가 될 것이라 확신합니다.

3. 서버 컴포넌트와 엣지 컴퓨팅: 클라이언트 너머로 확장되는 영역

Next.js의 App Router와 서버 컴포넌트(RSC)를 프로젝트에 도입하면서, 프론트엔드 개발자가 다뤄야 할 경계가 브라우저를 넘어 서버와 엣지(Edge) 영역으로 퍼지고 있음을 체감합니다.

  • 서버 이전: 무거운 연산을 클라이언트 브라우저가 아닌 서버에서 처리하여 사용자 기기의 부담을 줄임
  • 엣지 런타임(Edge Runtime): Vercel Edge, Cloudflare Workers 등을 활용해 사용자와 가장 가까운 위치의 서버에서 0.1초 만에 응답을 보내는 초저지연 성능 최적화

이제 프론트엔드 개발자는 단순히 "브라우저에서 어떻게 보일까?"만 고민해서는 안 되며, "이 로직을 클라이언트, 서버, 엣지 중 어디서 실행하는 것이 가장 효율적인가?"를 판단해야 합니다.

nextjs 터미널 실행화면

4. 앞으로를 준비하는 나의 로드맵

변화의 속도가 너무 빨라 가끔은 불안하기도 하지만, 결국 답은 '기본기'로 돌아가는 것이었습니다. 제가 다짐한 미래 준비 로드맵은 다음과 같습니다.

  1. AI를 '적'이 아닌 '최고의 부사수'로 활용하기: 단순히 코드를 써달라고 하는 수준을 넘어, 내 개발 흐름을 학습시키고 테스트 코드를 자동 생성하게 만드는 등 AI 생산성을 극대화하기
  2. 시스템 언어(Rust) 찍어먹어 보기: 당장 실무에 전면 도입하지 않더라도, 메모리 구조와 저수준 연산을 이해하여 Wasm 시대를 대비하기
  3. 탄탄한 기본기 유지하기: 프레임워크와 AI 툴은 계속 바뀌지만, 브라우저 렌더링 원리, 네트워크(HTTP/캐싱), 자료구조 같은 핵심 원리는 절대 변하지 않습니다.

5. 글을 마치며

AI가 코드를 대신 짜주는 시대일수록, 아이러니하게도 "진짜 실력 있는 개발자"의 가치는 더욱 높아지고 있습니다. AI가 만든 결과물을 검증하고 책임지는 것은 결국 인간 개발자의 몫이기 때문입니다.

기술의 유행에 휘둘려 조급해하기보다는, 기본기를 단단히 다지면서 새로운 툴을 적극적으로 받아들이는 말랑말랑한 사고를 유지하는 것. 그것이 변화무쌍한 프론트엔드 생태계에서 오래도록 즐겁게 개발할 수 있는 비결이 아닐까 싶습니다.

지난번 쇼핑몰 프로젝트를 진행할 때의 일입니다. 테스트할 때 고화질 상품컷을 몇 십 장씩 업로드하면서 메인 페이지 접속 속도가 터무니없이 느려지는 현상이 발생했습니다.

크롬 개발자 도구를 켜서 네트워크(Network) 탭을 확인해 보니, 메인 페이지 전체 용량 8MB 중 무려 6.5MB가 고용량 PNG/JPEG 이미지였습니다. 메인 배너 이미지 하나 용량이 3MB에 달하다 보니, 모바일 환경에서는 화면이 뜨는 데 4~5초 이상 걸려 사용자 이탈률이 치솟고 있었죠.

그때 깨달았습니다. 수백 줄의 자바스크립트 로직을 줄이는 것보다 이미지 한 장의 용량을 3MB에서 100KB로 압축하는 것이 훨씬 더 파괴적인 성능 개선을 가져온다는 사실을요. 오늘은 제가 직접 프로젝트에 적용해 보고 효과를 크게 봤던 이미지 최적화 3대 전략을 공유해 봅니다.

1. 차세대 포맷(WebP, AVIF)으로 변환: 용량이 반토막 나는 마법

가장 먼저 한 작업은 수십 십 개의 JPEG, PNG 파일들을 차세대 포맷인 WebPAVIF로 변환하는 것이었습니다.

  • WebP: PNG처럼 투명도(Alpha)를 지원하면서도, JPEG 대비 용량이 25~35% 이상 감소합니다.
  • AVIF: 현존 최강의 압축률을 자랑하며, WebP보다도 20% 더 가볍습니다.

구형 브라우저 분기 처리를 위해 <picture> 태그를 활용해 코드를 변경했습니다.

HTML
 
<!-- 브라우저가 지원하는 최신 포맷을 알아서 골라 로드하도록 분기 처리 -->
<picture>
  <source srcset="banner.avif" type="image/avif" />
  <source srcset="banner.webp" type="image/webp" />
  <img src="banner.jpg" alt="메인 기획전 배너" />
</picture>

이렇게 포맷만 바꿔줬는데도 이미지 전체 용량이 6.5MB에서 1.8MB로 대폭 줄어드는 것을 눈으로 확인했습니다.

squoosh.app을 이용해 이미지 용량 대폭 감소

2. 지연 로딩(Lazy Loading)과 나의 실수: LCP 이미지는 건드리지 마라!

화면을 아래로 스크롤하기 전까지는 보이지도 않는 하단 상품 이미지들까지 한 번에 로드할 필요가 전혀 없었습니다. 그래서 HTML5 표준 속성인 loading="lazy"를 도입했습니다.

HTML
 
<!-- 화면 뷰포트에 가까워지면 그때 다운로드를 시작합니다 -->
<img src="product-detail.webp" loading="lazy" alt="상품 상세 설명" />

🚨 내가 겪었던 삽질: 메인 배너에 lazy를 걸었다가 점수가 깎이다?

여기서 욕심을 내서 메인 최상단 배너 이미지에도 loading="lazy"를 적용했었는데, 오히려 구글 Lighthouse의 LCP(Largest Contentful Paint - 가장 큰 콘텐츠가 뜨는 시간) 점수가 더 떨어지는 기현상이 일어났습니다.

알고 보니 첫 화면에 바로 보여야 하는 핵심 이미지에는 절대 lazy를 걸면 안 되는 것이었습니다. 브라우저가 로딩을 오히려 미루기 때문이죠. 상단 배너에는 loading="eager"와 함께 fetchpriority="high" 속성을 주어 우선순위를 높여주는 것이 정답이었습니다.

3. responsive images (srcset): 모바일 사용자 데이터 아껴주기

데스크톱용 1920px 너비의 이미지를 모바일 사용자(너비 390px)에게 전송하는 건 명백한 데이터 낭비였습니다. 기기 해상도에 맞춰 최적화된 크기를 전달하기 위해 srcset 속성을 적용했습니다.

HTML
 
<img
  src="banner-small.webp"
  srcset="banner-small.webp 500w, banner-medium.webp 1000w, banner-large.webp 2000w"
  sizes="(max-width: 600px) 480px, 800px"
  alt="메인 배너"
/>

이렇게 설정해 두니 브라우저가 알아서 현재 모바일 기기인지, 데스크톱인지 파악하여 가장 딱 맞는 크기의 이미지만 쏙 골라서 다운로드하더군요.

Lighthouse Performance 점수

4. 실무 이미지 최적화 체크리스트

프로젝트를 진행하며 정리한 저만의 최적화 루틴입니다.

  1. 상세 페이지 이미지 용량 제어: 웹용 이미지는 무조건 장당 200KB 이하로 맞추기
  2. Layout Shift(CLS) 방지: <img> 태그에 width와 height 속성을 명시해서 이미지가 늦게 떠도 화면이 덜컹거리지 않게 영역 미리 잡기
  3. EXIF 메타데이터 제거: 카메라 촬영 정보, 위치 정보 등 불필요한 메타데이터는 압축 툴로 싹 지워주기

5. 글을 마치며

이미지 최적화는 단순히 기술적인 도전을 넘어, 우리 서비스를 이용하는 사용자의 데이터와 시간을 아껴주는 작업이었습니다.

실제로 이미지 다이어트 작업을 마친 뒤 메인 페이지 체감 로딩 속도가 3초 이상 단축되었고, 팀 내부에서도 만족도가 매우 높았습니다. 만약 지금 운영 중인 사이트나 블로그가 답답하게 느껴진다면, 자바스크립트 코드를 건드리기 전에 먼저 이미지들부터 가볍게 다이어트시켜 보시는 걸 강력히 추천합니다!

팀 프로젝트에 처음 합류했을 때, 가장 저를 당황스럽게 만들었던 건 로직 에러가 아닌 'Git Merge 충돌(Conflict)'이었습니다. 분명히 저는 로그인 로직 코드 한 줄만 고쳤는데, PR(Pull Request)을 올려보니 파일 전체에 빨간 줄이 그어지며 충돌이 난 것이죠.

원인을 찾아보니 제 에디터는 파일 저장 시 작은따옴표(')로 자동 변환하고 있었고, 동료 개발자의 에디터는 큰따옴표(")와 세미콜론을 강제로 붙이고 있었습니다. 코드 리뷰 시간에 로직을 검토하기도 전에 "여기 들여쓰기 탭 간격 고쳐주세요", "따옴표 통일해 주세요" 같은 스타일 지적 주고받기로 수십 분을 허비하곤 했습니다.

이 소모적인 논쟁을 원천 차단하고 "저장(Ctrl+S) 버튼만 누르면 알아서 코드 스타일에 대한 약속을 지켜주는 환경"을 구축했던 경험을 정리해 봅니다.

1. ESLint와 Prettier: 파수꾼과 재단사 역할 분담하기

처음엔 ESLint 하나만 설치하면 포맷팅까지 다 되는 줄 알고 썼다가, 두 도구의 규칙이 서로 싸우면서 에러를 뿜어내는 현상을 겪었습니다. 이 둘은 역할이 엄연히 다릅니다.

  • ESLint (Linter): 코드의 논리적 질(Quality)을 감시합니다.
    • "선언해 두고 안 쓰는 변수가 있어요!", "리액트 훅의 규칙을 어겼어요!" 같은 버그 유발 가능성을 경고합니다.
  • Prettier (Formatter): 코드의 시각적 스타일(Style)을 재단합니다.
    • 줄 바꿈, 들여쓰기 2칸, 마지막 콤마(Trailing Comma) 등 외형을 통일해 줍니다.

2. 두 도구 충돌 없이 한 번에 세팅하기 (실무 설정)

제가 프로젝트에서 가장 안정적으로 사용 중인 패키지 조합입니다. 핵심은 eslint-config-prettier를 넣어서 Prettier가 담당하는 스타일 규칙에 ESLint가 태클 걸지 않도록 충돌을 끄는 것입니다.

 

💡 패키지 설치

npm install -D eslint prettier eslint-config-prettier eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser

 

💡 .eslintrc.json 설정 (리액트+타입스크립트 프로젝트)

{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react-hooks/recommended",
    "prettier" // ⭐ 반드시 가장 마지막에 넣어 스타일 규칙 충돌을 막아줍니다!
  ],
  "rules": {
    "no-unused-vars": "warn",
    "react/prop-types": "off",
    "react/react-in-jsx-scope": "off"
  }
}

3. "저장만 하면 알아서 고쳐지게": 팀 공통 VS Code 설정

팀원마다 각자 VS Code 플러그인 설정이 달라서 규칙이 깨지는 것을 막기 위해, 프로젝트 루트 디렉토리에 .vscode/settings.json 파일을 만들어서 Git에 공유했습니다.

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}

이 설정을 공유하고 난 뒤, 팀원 모두가 Ctrl + S (저장)를 누르는 순간 자동으로 ESLint 규칙에 맞게 코드가 정돈되기 시작했습니다. 포맷팅 때문에 리뷰 코멘트를 남길 일이 100% 사라졌죠!

4. Git Hooks(Husky)로 '규칙 어긴 코드' 커밋 원천 봉쇄하기

설정을 공유해도 간혹 VS Code 확장 프로그램이 꺼져있거나, CLI에서 그대로 커밋을 올려서 엉망인 코드가 메인 브랜치로 들어오는 경우가 있었습니다. 이를 방지하기 위해 Huskylint-staged를 도입했습니다.

git commit 명령어를 내리는 순간, Husky가 작동하여 스테이징된(Staged) 파일들만 린트 검사를 돌립니다. 만약 ESLint 에러를 통과하지 못하면 아예 Git 커밋 자체가 거부되도록 강제했습니다.

5. 글을 마치며

ESLint와 Prettier를 제대로 세팅하는 데 들어간 시간은 불과 20~30분이었습니다. 하지만 이 초반 세팅 덕분에 저희 팀은 수개월 간의 프로젝트 기간 동안 단 한 번도 코드 스타일로 인한 Git 충돌이나 코드 리뷰 소모전을 겪지 않았습니다.

좋은 아키텍처와 협업의 시작은 거창한 시스템이 아니라, "팀원 모두가 일관된 정갈한 코드를 유지하는 시스템"을 만드는 데에 있습니다. 혼자 코딩하든 팀으로 코딩하든, 프로젝트 시작 시점에 이 두 도구를 세팅하는 습관을 들이시는 걸 강력히 추천합니다!

팀 프로젝트에서 배포를 진행한 직후, 팀원이 당황스러운 소식을 전해왔습니다. "아까 새로 고친 팝업창 버튼은 잘 되는데, 기존에 잘 되던 [로그인]이랑 [장바구니 담기] 기능이 클릭이 안 돼요!"

분명 내 파트 코드만 살짝 고치고 수동으로 클릭해 본 뒤 Merge했었는데, 공통으로 쓰이던 유틸리티 함수와 리액트 상태(State)가 꼬이면서 사이드 이펙트(Side Effect)가 발생했던 것입니다. 배포할 때마다 식은땀을 흘리며 깨달았습니다.

"프로젝트 규모가 커질수록, 사람이 수동으로 버튼을 하나씩 눌러보며 버그를 검증하는 것엔 한계가 있구나."

내가 작성하거나 고친 코드가 기존 기능에 영향을 미치지 않는다는 확신을 얻고, 마음 편히 배포하게 만들어준 테스트 자동화 구축기를 정리해 봅니다.

1. 현실적인 테스트 전략 (테스트 피라미드)

모든 코드에 테스트를 만드느라 정작 기능 구현보다 테스트 짜는 시간이 더 오래 걸리면 안 되기에, 저희 팀 프로젝트에 맞게 우선순위를 정해 테스트 피라미드를 구축했습니다.

  • 1단계: 유닛 테스트 (Unit Test - Jest)
    • UI와 상관없는 순수 핵심 로직(할인율 계산, 날짜 포맷팅, 복잡한 데이터 변환 함수)만 빠르게 검증합니다.
  • 2단계: 통합 테스트 (Integration Test - React Testing Library)
    • 내가 만든 컴포넌트에서 "사용자가 폼을 입력하고 버튼을 눌렀을 때 원하는 화면이 잘 나타나는가?"를 검증합니다.
  • 3단계: E2E 테스트 (End-to-End Test - Cypress)
    • 실제 브라우저를 띄워 [메인 ➔ 로그인 ➔ 장바구니 ➔ 결제]로 이어지는 핵심 서비스 동선만 통째로 검증합니다.

2. Jest와 RTL로 단단한 컴포넌트 만들기 (실제 적용 코드)

리액트 프로젝트에 React Testing Library(RTL)를 적용하면서 지킨 원칙은 "컴포넌트의 내부 state가 아니라, 실제 사용자의 행동을 기준으로 테스트하는 것"이었습니다.

 
// LoginForm.test.tsx - 로그인 폼 테스트 코드
import { render, screen, fireEvent } from '@testing-library/react';
import LoginForm from './LoginForm';

test('아이디와 비밀번호를 모두 입력해야만 로그인 버튼이 활성화된다', () => {
  render(<LoginForm />);
  
  const idInput = screen.getByLabelText(/아이디/i);
  const pwInput = screen.getByLabelText(/비밀번호/i);
  const submitButton = screen.getByRole('button', { name: /로그인/i });

  // 1. 처음엔 버튼이 비활성화(disabled) 상태여야 함
  expect(submitButton).toBeDisabled();

  // 2. 사용자가 폼에 타이핑을 진행
  fireEvent.change(idInput, { target: { value: 'yumina' } });
  fireEvent.change(pwInput, { target: { value: 'password123!' } });

  // 3. 두 값이 제대로 들어가면 버튼이 활성화되는지 검증
  expect(submitButton).toBeEnabled();
});

Jest 테스트가 통과된 화면 스크린샷

3. Cypress로 팀의 '핵심 서비스 동선' 구하기

유닛 테스트가 부분 부품을 검사한다면, Cypress는 "진짜 사용자가 들어와서 서비스를 끝까지 이용할 수 있는가"를 브라우저에서 직접 시뮬레이션해 줍니다.

// cypress/e2e/checkout.cy.js - 메인 플로우 검증
describe('핵심 서비스 플로우 검증', () => {
  it('상품 페이지에서 장바구니에 담고 결제 페이지까지 문제없이 이동한다', () => {
    cy.visit('/products/1');
    cy.contains('장바구니 담기').click();
    
    // 장바구니 카운트가 올라갔는지 검증
    cy.get('.cart-badge').should('contain', '1');
    
    cy.visit('/cart');
    cy.contains('주문하기').click();
    
    // 최종 결제 URL로 잘 넘어가는지 검증
    cy.url().should('include', '/checkout');
  });
});

Cypress를 붙여둔 덕분에, 배포 전 버튼들을 일일이 마우스로 클릭해 보지 않고도 명령어 하나로 전체 동작을 5초 만에 검증할 수 있게 되었습니다.

Cypress 테스트 러너 실행 결과 화면 스크린샷

 

4. TDD(테스트 주도 개발)에 대한 나의 생각

프로젝트 일정이 빡빡할 때는 모든 코드를 TDD로 짜는 것은 어려웠습니다. 그래서 제가 찾은 타협점은 다음과 같습니다.

  • TDD 적용 영역: 정산/결제 금액 계산 로직, 데이터 변환 유틸 함수 (버그 나면 치명적인 곳)
  • 일반 개발 후 테스트 적용 영역: 컴포넌트 레이아웃, CSS 스타일링, 단순 안내 페이지

5. GitHub Actions로 자동 배포 검문소 만들기

테스트 코드를 짜두기만 하고 안 돌리면 의미가 없어서, GitHub Actions를 활용해 가상 검문소를 만들었습니다.

팀원이 PR(Pull Request)을 올릴 때마다, GitHub 서버가 자동으로 Jest와 Cypress를 먼저 실행합니다. 여기서 테스트가 단 하나라도 실패하면 메인 브랜치에 Merge 자체가 되지 않도록 막아두었습니다.

6. 글을 마치며

테스트 코드는 단순히 '코드의 품질'을 높이는 수단이 아닙니다. "배포할 때 오는 불안함을 없애주는 최고의 안전장치"입니다.

내가 수정한 코드가 기존 기능을 깨뜨리지 않았다는 확신이 생기니, 새로운 기능을 시도하거나 리팩토링을 진행할 때도 주저 없이 코드를 고칠 수 있게 되었습니다.

팀 프로젝트나 개인 프로젝트를 진행하시면서 배포에 불안함을 느끼셨다면, 오늘 밤 가장 핵심이 되는 유틸 함수에 작은 유닛 테스트 하나부터 작성해 보세요!

웹 애플리케이션의 복잡도가 높아지면서 프론트엔드 보안의 중요성도 그 어느 때보다 커졌습니다. 보안 사고는 서비스의 신뢰도를 한순간에 무너뜨릴 뿐만 아니라 사용자의 개인정보 유출로 직결됩니다. 오늘은 프론트엔드 개발자가 반드시 알고 있어야 할 두 가지 핵심 공격 기법인 XSSCSRF의 원리를 파악하고, 이를 방어하기 위한 실무 전략을 알아보겠습니다.


1. XSS (Cross-Site Scripting): 내 사이트에 남이 짠 코드가?

XSS는 공격자가 악성 스크립트를 웹 페이지에 삽입하여, 다른 사용자의 브라우저에서 해당 스크립트가 실행되게 만드는 공격입니다.

1.1 공격의 위험성

스크립트가 실행되면 공격자는 사용자의 세션 쿠키를 가로채거나, 사용자를 대신해 게시글을 작성하고, 민감한 정보를 외부 서버로 전송할 수 있습니다.

1.2 프론트엔드 방어 전략

React와 같은 최신 프레임워크는 기본적으로 렌더링 시 데이터를 이스케이프(Escape) 처리하여 XSS를 방지합니다. 하지만 다음 상황에서는 위험에 노출됩니다.

  • dangerouslySetInnerHTML 사용 지양: HTML을 직접 렌더링해야 한다면 반드시 dompurify 같은 라이브러리로 살균(Sanitize) 과정을 거쳐야 합니다.
  • 사용자 입력값 검증: URL 쿼리 스트링이나 폼 입력값을 다룰 때 항상 유효성을 검사하세요.
  • CSP(Content Security Policy) 설정: 브라우저에게 "이 사이트에서 실행할 수 있는 스크립트 출처는 여기뿐이야"라고 알려주는 보안 헤더를 설정하세요.

2. CSRF (Cross-Site Request Forgery): 내가 안 보낸 요청이 서버로?

CSRF는 사용자가 자신의 의지와 무관하게 공격자가 의도한 행위(비밀번호 변경, 결제 등)를 특정 웹사이트에 요청하게 만드는 공격입니다. 사용자가 로그인되어 있는 브라우저의 '쿠키'를 이용하는 것이 핵심입니다.

2.1 공격 원리

사용자가 은행 사이트에 로그인된 상태에서 공격자가 만든 악성 사이트에 접속하면, 그 사이트 내부의 숨겨진 폼이 은행 서버로 '송금 요청'을 보냅니다. 브라우저는 은행 쿠키를 자동으로 함께 전송하므로, 서버는 사용자의 정상적인 요청으로 착각하게 됩니다.

2.2 프론트엔드 방어 전략

  • SameSite 쿠키 속성: 쿠키 설정 시 SameSite=Lax 또는 Strict를 사용하여 타 도메인에서의 쿠키 전송을 제한하세요.
  • CSRF 토큰: 서버에서 발급한 일회용 토큰을 요청 헤더에 담아 보내 서버가 검증하게 하세요.
  • Custom Header 사용: X-Requested-With 같은 커스텀 헤더를 요구하면 브라우저의 기본 폼 제출로는 요청을 보낼 수 없어 방어에 유리합니다.

3. 안전한 토큰 관리: LocalStorage vs Cookie

많은 개발자가 고민하는 주제입니다. "JWT(JSON Web Token)를 어디에 저장해야 할까요?"

3.1 LocalStorage (XSS에 취약)

  • 장점: 사용이 간편하고 CSRF 공격으로부터 자유롭습니다.
  • 단점: 자바스크립트로 접근이 가능하므로 XSS 공격에 노출되면 토큰이 그대로 탈취됩니다.

3.2 HttpOnly Cookie (CSRF에 취약)

  • 장점: 자바스크립트 접근이 불가능하여 XSS로부터 안전합니다.
  • 단점: 브라우저가 자동으로 요청에 포함하므로 CSRF 공격 대상이 됩니다.

최선의 선택: HttpOnly 및 Secure 속성이 적용된 쿠키를 사용하되, SameSite 속성CSRF 토큰을 병행하여 CSRF 공격을 방어하는 것이 가장 견고한 보안 모델로 평가받습니다.


4. HTTPS와 보안 헤더 설정

네트워크 레벨에서의 보안도 필수입니다.

  • HSTS (HTTP Strict Transport Security): 브라우저가 해당 사이트에 항상 HTTPS로만 접속하도록 강제합니다.
  • X-Frame-Options: 내 사이트가 다른 사이트의 <iframe> 안에 들어가는 것을 막아 클릭재킹(Clickjacking) 공격을 방어합니다.

5. 결론: 보안은 체크리스트가 아닌 프로세스입니다

프론트엔드 보안은 단 하나의 라이브러리 설치로 끝나지 않습니다. 코드를 짤 때마다 "이 입력값이 안전한가?", "이 요청이 신뢰할 수 있는가?"를 끊임없이 질문해야 합니다.

오늘의 요약:

  1. XSS 방어: 데이터 이스케이프를 믿되, HTML 직접 삽입 시에는 반드시 살균하세요.
  2. CSRF 방어: SameSite 쿠키와 커스텀 헤더/토큰을 활용하세요.
  3. 저장소 선택: 민감한 정보는 자바스크립트가 만질 수 없는 곳에 두세요.
  4. CSP 도입: 보안 헤더를 통해 브라우저의 방어 능력을 극대화하세요.

+ Recent posts