이번에 개발하는 케어나는 사용자의 건강검진 결과를 분석하고 관리해 주는 서비스다.
이 서비스는 오늘 할 일이나 점심 메뉴와 같은 가벼운 데이터가가 아니라, 누군가의 혈압, 당뇨 수치 등의 건강정보를 다룬다. 만약 실수로 이 정보가 유출된다면? 단순한 버그 수정으로는 되돌릴 수 없는 사고가 된다😢
프론트엔드는 본질적으로 누구나 코드를 뜯어볼 수 있는 환경이다. 그리고 브라우저는 확장 프로그램이나 악성 스크립트 등 공격자가 개입할 여지가 너무 많다. 그래서 본격적인 개발에 앞서, 우리 팀만의 프론트엔드 보안 대책을 생각해보기로 했다.
목표는 완벽한 방어가 아니라, (1) 유출 가능성을 줄이고, (2) 정보가 유출되더라도 피해를 최소화하며, (3) 재발을 어렵게 만드는 것이다.
1. 보호해야할 데이터와 주요 위협 사항
먼저 우리가 지켜야 할 것이 무엇인지, 그리고 무엇이 위험한지 정의했다.
1-1. 보호해야 할 데이터
- 의료/건강 정보: 검진 수치(혈압, LDL 등), 병원 정보, 검진일, 그리고 질환 예측 결과.
- 계정/세션 정보: 로그인 상태를 증명하는 쿠키, 사용자 고유 식별자.
1-2. 주요 위협
- XSS (Cross Site Scripting): 악성 스크립트가 실행되어 토큰을 훔치거나 화면의 데이터를 긁어가는 공격이다.
- 공용 기기 리스크: PC방이나 도서관 컴퓨터. 사용자가 로그아웃하지 않거나 브라우저 캐시, '뒤로 가기'를 통해 이전 사람의 검진 결과가 노출되는 위협이다.
- 로깅: 개발 편의를 위해 사용한 console.log나 GA, Sentry 같은 분석 도구에 민감 정보가 섞여 들어가는 경우다.
2. 흔적을 남기지 말기
프론트엔드 보안에서 가장 중요한것은 중요한 정보를 저장하지 않는 것이다.
2-1. 민감 데이터 localStorage / sessionStorage 사용 금지
- 문제점: localStorage는 XSS 공격이 성공하는 순간, window.localStorage한 줄로 정보가 유출될 수 있다. 위험.
- 대처법:
- 검진 결과(수치/해설)는 절대 영구 저장하지 않는다.
- 데이터는 화면에 필요한 동안만 메모리(React State, TanStack Query Cache)에 띄워둔다.
- 새로고침 하거나 탭을 닫으면 데이터가 날아가는 것을 버그가 아닌 보안 기능으로 정의한다.
- UX 트레이드오프: 새로고침 하면 데이터가 사라져서 불편하지 않을까 라는 의견이 있을 수 있다. 하지만 편의성보다 안전이 우선이므로 로딩 UI와 안내 문구를 다시 안전하게 불러오는 중입니다 라는 식으로 친절한 느낌의 문구를 띄워주는 것도 좋은 방법이라고 생각한다.
3. TanStack Query 정책: persist 금지
상태 관리 라이브러리로 TanStack Query를 쓰지만, 보안을 위해 일부 기능은 제한한다.
3-1. persist (로컬 저장) 금지
TanStack Query에는 캐시를 로컬 스토리지에 저장해서 오프라인에서도 보게 해주는 persistQueryClient 기능이 있다. 공용 pc에 검진 기록이 남을 수 있기 때문에 persist를 금지한다.
- 결정: 검진 결과 관련 Query에는 persist 옵션을 절대 켜지 않는다.
3-2. QueryClient 설정: 캐시를 짧게 가져가기
보안 설정은 아니지만, 메모리 상에도 민감 데이터가 너무 오래 남아있지 않도록 GC(Garbage Collection) 시간을 짧게 잡는것도 방법이다.
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 0, // 에러나면 즉시 실패 처리
staleTime: 0, // 데이터는 항상 신선하지 않다고 가정
gcTime: 1000 * 60 * 5, // 5분 지나면 메모리에서도 삭제
refetchOnWindowFocus: false, // 창 전환 시마다 다시 부르는 건 부하가 크니 끔
},
},
});
중요한 것은 데이터를 필요 이상으로 오랫동안 보관하고 있지 않는 것이다.
4. Network & Session
4-1. 토큰은 localStorage가 아니라 HttpOnly 쿠키로
- 이유: HttpOnly 속성이 붙은 쿠키는 자바스크립트(document.cookie)로 접근할 수 없다. XSS가 터져도 해커가 토큰을 탈취하기 어려워진다.
- 프론트의 역할: 쿠키 생성은 서버가 하지만, 프론트는 요청을 보낼 때 쿠키를 잘 실어 보내야 한다. credentials: 'include' 설정이 필수다.
// ky 라이브러리 예시
import ky from 'ky';
export const api = ky.create({
prefixUrl: '/api',
credentials: 'include', // 쿠키 전송을 위해 필수!
timeout: 10_000,
});
4-2. Cache-Control: no-store
공용 컴퓨터에서 로그아웃을 했는데, 뒤에 컴퓨터를 사용하는 사람이 '뒤로 가기'를 눌렀더니 내 검진표가 뜬다면. . . 몹시 곤란하다
이건 프론트에서 막기 힘들다. 브라우저가 자체적으로 캐싱하기 때문이다.
이것은 서버에서 API 응답 헤더에 cache-control: no-store 로 브라우저에서 해당 데이터를 캐시에 저장하지 않도록 설정해야한다.
5. React Ecosystem: XSS와 실패 경험 통제하기
5-1. dangerouslySetInnerHTML은 정말 위험할 때만
React는 기본적으로 텍스트를 이스케이프 처리해서 XSS를 막아준다. 하지만 dangerouslySetInnerHTML을 쓰는 순간 소용이 없어진다.
- 결정: 외부에서 받아온 HTML(예: 건강 팁 본문)을 렌더링해야 한다면, 반드시 DOMPurify로 Sanitize한 뒤에 넣는다.
- 코드 예시:
import DOMPurify from 'dompurify';
export function SafeHtml({ html }: { html: string }) {
// 스크립트 태그 등을 걷어낸다
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
5-2. ErrorBoundary로 내부 정보 숨기기
에러가 났을 때 화면에 Error: undefined is not... at /src/components/... 같은 스택 트레이스가 뜨면 사용자도 불안하고, 해커에게는 시스템 구조를 알려주는 힌트가 된다.
- 결정: ErrorBoundary를 사용해 에러 발생 시 일시적인 오류입니다 같은 Fallback만 보여준다. 절대 원본 에러 메시지를 화면에 출력하면 안된다.
6. Logging & Analytics: 값 대신 행동만 남기기
6-1. GA/Sentry에 수치 보내지 않기
디버깅을 위해서 console.log(userHealthData)를 남기거나, GA 이벤트에 label: '혈당 150'을 보내면 개인정보 유출이 일어나게 된다. (Google Analytics 정책상으로도 PII 전송은 계정 정지 사유이다.)
- 대안:
- 이벤트에는 행동만 남긴다. (예: 검진 상세 조회, 그래프 클릭)
- 구체적인 값은 절대 보내지 않는다. (예: 혈당, 몸무게, 이름 금지)
6-2. 만약 Sentry 도입 시 스크러빙(Scrubbing) 필수
Sentry는 에러 상황의 모든 걸 캡처하려고 한다. 이때 beforeSend 훅을 사용해서 민감한 정보가 서버로 날아가기 전에 필터링해야 한다.
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
beforeSend(event) {
// 요청 객체에 포함된 쿠키, 헤더, 바디 데이터는 지운다
if (event.request) {
delete event.request.cookies;
delete event.request.data;
delete event.request.headers;
}
return event;
},
});
7. 화면 캡쳐 방지
보안에 관련하여 팀원들과 가장 많이 논의했던 주제 중 하나는 캡처 방지였다.
금융 앱들처럼 캡처를 시도하면 "보안 정책에 따라 캡처할 수 없습니다"라는 토스트 메시지를 띄우고 검은색 화면만 나오게 하고 싶었다.
하지만 웹 환경의 보안 모델을 깊게 공부하면서, 웹에서 캡처 차단은 기술적으로 불가능하다는 결론에 도달했다.
그 이유와 차선책을 정리해보았다.
7-1. 왜 웹에서는 캡처를 막을 수 없을까? (Browser Sandbox)
Android나 iOS는 OS에게 보안 앱이니까 화면을 건들지말라고 요청할 수 있는 권한(FLAG_SECURE등)이 있다. 하지만 웹은 다르다.
- 샌드박스의 한계: 브라우저는 사용자의 안전을 위해 OS의 핵심 기능에 접근하지 못하도록 되어 있다.JavaScript는 브라우저가 렌더링한 DOM 내부만 제어할 수 있고, OS 차원에서 실행되는 스크린샷·화면 녹화 기능에는 접근하거나 제어할 권한이 없다.
- 렌더링 주체의 차이: 화면을 그리는 건 브라우저지만, 그 화면을 보고 있는 건 운영체제의의 윈도우 매니저다. JavaScript에서 어떤 이벤트를 막더라도, OS 레벨에서 발생하는 캡처 명령은 브라우저를 거치지 않고 실행된다.
- OTT의 캡처 방지에 대한 오해 : 넷플릭스 같은 OTT 서비스가 캡처를 막는 것처럼 보이는 이유는EME(Encrypted Media Extensions) 기반 DRM으로 비디오 스트림 자체만 암호화하기 때문이다.이는 영상 재생에 특화된 방식이며, HTML 텍스트나 차트처럼 DOM으로 렌더링되는 일반 웹 UI에는 적용할 수 없다.
결국 웹에서 캡쳐를 막는것은 기술적으로 불가능하다😭
7-2. 모바일 웹에서 위험한 순간 : 화면이 백그라운드로 전환될 때
캡처를 막을 수는 없지만, 모바일 웹 환경에서 민감 정보가 의도치 않게 노출되기 쉬운 순간은 분명히 존재한다.
사용자가 카페에서 검진 결과를 확인하다가 알림을 눌러 다른 앱으로 이동하거나 브라우저를 백그라운드로 보내는 순간이다.
이때 브라우저는 탭 상태를 유지하거나, 전환 직전의 화면을 잠시 노출할 수 있고
공용 장소에서는 이 화면이 그대로 노출될 수 있다.
전환되는 순간의 노출을 줄이기 위해 Page Visibility API를 활용하는 것도 방법이다.
import { useEffect } from 'react';
export function useSecurityBlur() {
useEffect(() => {
const handleVisibilityChange = () => {
// document.hidden이 true면, 브라우저가 백그라운드로 갔다는 뜻.
if (document.hidden) {
// 즉시 화면 전체를 로고나 블러 처리된 오버레이로 덮는다.
document.body.classList.add('secure-blur-overlay');
} else {
// 다시 돌아오면 오버레이를 제거한다.
document.body.classList.remove('secure-blur-overlay');
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
}, []);
}
이 훅을 적용하면 브라우저가 백그라운드로 전환되는 즉시 검진 결과 화면은 블러 처리되거나 서비스 로고로 덮인다.
그 결과, 앱 전환 직후나 탭 미리보기에서 민감한 수치가 그대로 노출되는 상황을 줄일 수 있다.
물론 이 방식이 스크린샷이나 화면 녹화를 완전히 막아주는 것은 아니다.
하지만 웹 환경이라는 기술적 한계 안에서, 실수로 인한 정보 노출을 줄이는 가장 현실적인 대책이라고 판단했다.
8. 마치며
프론트엔드 개발자로서 100% 완벽한 보안을 장담할 수는 없다. 하지만 적어도 브라우저에 흔적을 남기지 않고, 보안 사고가 났을 때 로그를 통해 유출되지 않도록 막고, 실패했을 때도 안전하게 실패하도록 하는것이 현재 상황에서 최선의 방법이라고 생각한다.
'FE' 카테고리의 다른 글
| biome , left-hook 설정 (0) | 2026.03.29 |
|---|---|
| 올바른 HTTP 클라이언트 선택하기 - fetch, Axios, Ky (1) | 2026.01.05 |
| React Suspense + Error Boundary로 안정적인 로딩/에러 핸들링 (1) | 2026.01.03 |
| 건강검진 결과 시각화 : 차트 라이브러리 선정 및 구현 전략 (0) | 2026.01.01 |
| PDF 업로드가 느린 이유와 런칭을 위한 성능 최적화 (0) | 2025.12.30 |