우리는 지금까지 React의 다양한 기능과 아키텍처, 성능 최적화 기법들에 대해 배웠습니다. 하지만 아무리 기능이 뛰어나고 빠른 애플리케이션이라도 보안이 취약하거나 안정적이지 못하다면 사용자들은 신뢰를 잃고 떠나갈 것입니다.
특히 사용자의 개인 정보나 금융 정보 등을 다루는 서비스라면 보안과 안정성은 더욱 중요합니다. 최근 SKT에서 일어난 유심 정보 유출 사건이나, 카카오의 서비스 장애와 같은 사례를 보면 알 수 있듯이, 보안과 안정성은 단순히 개발자의 선택이 아니라 기업의 생존과 직결되는 문제입니다.
프론트엔드 개발자도 서버 개발자만큼이나 보안과 안정성에 대한 책임감을 가지고 개발에 임해야 합니다.
XSS는 공격자가 악의적인 JavaScript 코드를 웹 페이지에 삽입하여, 해당 페이지를 방문한 다른 사용자의 브라우저에서 그 코드가 실행되도록 만드는 공격입니다.
이를 통해 공격자는 사용자의 쿠키(세션 정보)를 탈취하거나, 개인 정보를 훔쳐보거나, 사용자의 의도와 다른 행동을 수행하게 만들 수 있습니다.
가장 흔한 경우는 사용자가 입력한 내용을 그대로 화면에 보여줄 때 발생합니다.
예를 들어, 사용자가 자신의 프로필 소개를 입력하고 이를 다른 사용자에게 보여주는 기능이 있다고 가정해 봅시다.
🖐️
이 코드는 잘못된 코드 예시입니다. 실습 용도로만 사용하세요.
사용자 입력을 HTML로 직접 렌더링하기 위해 dangerouslySetInnerHTML을 부주의하게 사용하는 경우입니다.
import React from "react";function UserProfile({ bio }: { bio: string }) { // 사용자가 입력한 bio 내용을 HTML로 해석하여 그대로 삽입 // 절대 이렇게 사용하면 안 됩니다! return ( <div> <h3>소개</h3> <div dangerouslySetInnerHTML={{ __html: bio }} /> </div> );}const UserProfilePage = () => { // 만약 공격자가 bio에 다음과 같은 악성 스크립트를 입력했다면? const bio = '안녕하세요! 저는 웹 개발자입니다. <script>alert("XSS 공격 성공! 쿠키: " + document.cookie);</script>'; return ( <div> <h1>사용자 프로필</h1> <UserProfile bio={bio} /> {/* 이 컴포넌트가 렌더링되면, 다른 사용자의 브라우저에서 스크립트가 실행되어 경고창이 뜨고 쿠키 정보가 노출될 수 있습니다. */} </div> );};export default UserProfilePage;
dangerouslySetInnerHTML 렌더링 시
위 예시처럼 악성 스크립트가 포함된 사용자 입력이 필터링 없이 dangerouslySetInnerHTML을 통해 렌더링되면, 해당 페이지를 보는 다른 사용자의 브라우저에서 그 스크립트가 실행되어 쿠키 정보 탈취 등 심각한 보안 문제로 이어질 수 있습니다.
React는 JSX에서 중괄호 {} 안에 들어가는 모든 내용을 기본적으로 문자열로 취급하고, HTML 태그나 스크립트로 해석될 수 있는 특수 문자들(예: <, >, &)을 자동으로
이스케이프(escape)
처리합니다.
즉, <script> 태그를 문자열로 넣어도 브라우저는 이를 일반 텍스트로 인식하여 실행하지 않습니다.
import React from "react";function UserProfile({ bio }: { bio: string }) { // 사용자가 입력한 bio 내용을 HTML로 해석하여 그대로 삽입 // 절대 이렇게 사용하면 안 됩니다! return ( <div> <h3>소개</h3> <div>{bio}</div> </div> );}const UserProfilePage = () => { // 만약 공격자가 bio에 다음과 같은 악성 스크립트를 입력했다면? const bio = '안녕하세요! 저는 웹 개발자입니다. <script>alert("XSS 공격 성공! 쿠키: " + document.cookie);</script>'; return ( <div> <UserProfile bio={bio} /> {/* 이 컴포넌트가 렌더링되면, 다른 사용자의 브라우저에서 스크립트가 실행되어 경고창이 뜨고 쿠키 정보가 노출될 수 있습니다. */} </div> );};export default UserProfilePage;
문자열로 취급
따라서 가능한 한 dangerouslySetInnerHTML 사용을 피하고, 사용자 입력은 항상
CSP는 브라우저가 어떤 출처의 리소스(스크립트, 스타일, 이미지 등)를 로드하고 실행할 수 있는지 제어하는 HTTP 응답 헤더입니다.
서버에서 적절한 CSP 헤더를 설정하면, 예상치 못한 출처의 스크립트(XSS 공격 포함) 실행을 브라우저 레벨에서 차단할 수 있어 매우 효과적인 방어 수단입니다.
예를 들어,
사용자가 mybank.com에 로그인되어 있는 상태라고 가정해 봅시다.
이때 사용자가 악의적인 웹사이트 evil.com을 방문합니다.
evil.com에는 다음과 같은 코드가 숨겨져 있을 수 있습니다.
🖐️
이 코드는 잘못된 코드 예시입니다. 실습 용도로만 사용하세요.
<!-- 사용자는 이 이미지를 보지만, 브라우저는 mybank.com으로 요청을 보낸다 --><img src="https://mybank.com/transfer?to=hacker&amount=1000000" width="1" height="1" /><!-- 또는, 자동으로 제출되는 숨겨진 폼 --><form id="csrf-form" action="https://mybank.com/change-password" method="POST" style="display:none;"> <input type="hidden" name="newPassword" value="hacked123" /></form><script>document.getElementById('csrf-form').submit();</script>
사용자가 evil.com을 로드하면, 브라우저는 사용자의 mybank.com
로그인 쿠키를 자동으로 포함
하여 저 이미지 URL이나 폼 제출 요청을 mybank.com 서버로 보냅니다.
만약 mybank.com 서버가 이 요청이 정말 사용자가 의도한 것인지 검증하는 장치가 없다면, 공격자는 사용자의 계정으로 돈을 이체하거나 비밀번호를 변경하는 등의 악의적인 행동을 성공시킬 수 있습니다.
최신 브라우저는 쿠키의 SameSite 속성을 지원하여 CSRF 공격을 완화하는 데 도움을 줍니다.
SameSite=Strict
쿠키가 현재 사이트와 동일한 사이트에서 시작된 요청에만 전송됩니다.
외부 사이트에서 시작된 요청(예: evil.com에서 mybank.com으로)에는 쿠키가 전혀 전송되지 않아 강력한 CSRF 방어 효과가 있습니다.
단, 외부 사이트에서 내 사이트로 링크를 통해 이동하는 정상적인 경우에도 쿠키가 전송되지 않을 수 있습니다.
SameSite=Lax
Strict보다는 약간 완화된 설정입니다. 링크 클릭(<a> 태그), GET 메서드 폼 제출 등 안전하다고 판단되는 일부 최상위 레벨 탐색 시에는 다른 사이트에서 시작된 요청이라도 쿠키를 전송합니다.
대부분의 CSRF 공격(POST, <img> 등)은 막을 수 있으며, 사용자 경험을 크게 해치지 않아 기본값으로 권장되는 경우가 많습니다.
SameSite=None; Secure
모든 요청(동일 사이트, 다른 사이트)에 쿠키를 전송합니다. 반드시 Secure 속성(HTTPS에서만 쿠키 전송)과 함께 사용해야 합니다.
서드파티 컨텍스트에서 쿠키가 필요한 경우(예: 임베디드 콘텐츠 인증)에만 제한적으로 사용해야 합니다.
서버에서 중요한 세션 쿠키에 SameSite=Lax 또는 Strict 속성을 설정하는 것이 CSRF 예방에 큰 도움이 됩니다. 프론트엔드 개발자는 이 속성이 제대로 설정되어 있는지 확인하고 이해하는 것이 좋습니다.
요청 헤더의 Referer 값을 확인하여 요청이 허용된 도메인에서 시작되었는지 검사하는 방법입니다.
하지만 Referer 헤더는 사용자가 비활성화하거나 프록시 등에 의해 누락될 수 있어 완전한 방어책은 아닙니다.
커스텀 요청 헤더 사용
서버에서 특정 커스텀 헤더(예: X-Requested-With)가 포함된 요청만 허용하는 방식입니다.
브라우저의 동일 출처 정책(Same-Origin Policy) 때문에 다른 도메인에서는 JavaScript로 커스텀 헤더를 추가한 요청을 보내기 어렵다는 점을 이용합니다.
하지만 CORS 설정이 잘못되어 있다면 우회될 수 있습니다.
Q: "CSRF 공격이란 무엇이고 어떻게 예방할 수 있나요?"
A: 공격 원리, 예방책(CSRF 토큰, SameSite 쿠키)의 원리, 그리고 프론트엔드 개발자의 역할을 함께 설명
: 보통 만료 시간이 짧으며, API 요청 시 사용됩니다. 메모리(JavaScript 변수)에 저장하는 것이 비교적 안전합니다. LocalStorage나 SessionStorage는 XSS 공격에 취약할 수 있습니다.
Refresh Token
: 만료 시간이 길며, Access Token이 만료되었을 때 새 Access Token을 발급받는 데 사용됩니다. 보안상 매우 민감하므로, 가능하다면 서버 측 세션과 연결된 HttpOnly, Secure, SameSite=Strict (또는 Lax) 속성의 쿠키에 저장하여 프론트엔드 JavaScript가 직접 접근할 수 없도록 하는 것이 가장 안전합니다. 이것이 어렵다면 메모리에 저장하고 다른 보안 수단을 강구해야 합니다.
API 요청 시 토큰 사용
보호된 API를 호출할 때마다 프론트엔드는 저장된 Access Token을 HTTP 요청 헤더의 Authorization 필드에 담아 보냅니다 (예: Authorization: Bearer YOUR_ACCESS_TOKEN).
토큰 갱신
API 요청 시 Access Token이 만료되었다는 응답(예: 401 Unauthorized)을 받으면, 프론트엔드는 (HttpOnly 쿠키에 저장된 경우 브라우저가 자동으로 보내거나, 메모리에 저장된 경우 직접) Refresh Token을 사용하여 백엔드에게 새 Access Token 발급을 요청합니다. 백엔드가 새 토큰을 발급해주면 이를 다시 저장하고 실패했던 API 요청을 재시도합니다.
로그아웃
프론트엔드는 저장된 토큰(메모리, 쿠키 등)을 삭제합니다.
필요하다면, 인가 서버의 로그아웃 엔드포인트로 사용자를 리디렉션하여 외부 서비스에서도 로그아웃 처리를 완료합니다.
백엔드가 토큰 응답 시 Refresh Token을 HttpOnly, Secure, SameSite=Strict (또는 Lax) 속성을 가진 쿠키로 설정하여 브라우저에 저장시킵니다.
HttpOnly 속성 때문에 클라이언트 JavaScript에서는 이 쿠키에 접근할 수 없어 XSS 공격으로 탈취가 불가능합니다.
Secure는 HTTPS에서만 쿠키가 전송되도록 하고, SameSite는 CSRF 공격을 방어합니다.
프론트엔드의 역할
: 프론트엔드는 이 쿠키를 직접 읽거나 관리할 필요가 없습니다. Access Token 만료 시, 백엔드의 특정 토큰 갱신 엔드포인트(예: /api/refresh_token)를 호출하기만 하면 됩니다. 브라우저는 해당 엔드포인트로 요청을 보낼 때 자동으로 HttpOnly 쿠키(Refresh Token 포함)를 함께 전송합니다. 백엔드는 이 쿠키를 읽어 새 Access Token을 발급하고 응답으로 보내줍니다.
서버 측 쿠키 설정 예시 (Node.js/Express)
// 백엔드에서 로그인 성공 또는 토큰 발급 시res.cookie('refreshToken', receivedRefreshToken, { httpOnly: true, // JavaScript 접근 불가 secure: true, // HTTPS에서만 전송 sameSite: 'lax', // 또는 'strict' maxAge: 7 * 24 * 60 * 60 * 1000 // 예: 7일 유효기간});res.json({ accessToken: receivedAccessToken }); // Access Token은 JSON 응답으로 전달
Access Token과 마찬가지로 JavaScript 변수(상태 관리 라이브러리, Context API 등)에 저장합니다.
구현이 간단하고 CSRF에는 비교적 안전합니다.
브라우저 새로고침 시 토큰이 소실
되어 로그인이 풀립니다. 이를 해결하기 위해 LocalStorage/SessionStorage에 저장하는 경우가 있는데, 이는
XSS 공격에 매우 취약
하므로 Refresh Token 저장소로는 절대 권장되지 않습니다.
또한, 메모리에 저장된 토큰도 XSS 공격자가 특정 시점에 메모리 덤프 등을 시도할 경우 이론적으로 탈취 가능성이 존재합니다.
새로고침 문제 해결 시도 (덜 안전한 방법)
: 일부에서는 새로고침 시 로그인을 유지하기 위해 Refresh Token을 LocalStorage에 저장하고, 앱 시작 시 LocalStorage에서 읽어와 메모리에 로드하는 방식을 사용하기도 합니다.
하지만 이는 Refresh Token을 XSS 위험에 노출시키는 행위이므로 보안상 권장되지 않습니다. 만약 불가피하게 사용한다면 XSS 방어에 극도로 신경 써야 합니다.
👨💻
가능하다면 반드시 서버 측 HttpOnly 쿠키 방식을 사용하는 것이 Refresh Token을 안전하게 관리하는 최선의 방법입니다.
Q: "OAuth 2.0 인가 코드 플로우에서 프론트엔드의 역할은 무엇인가요?" 또는 "Access Token은 어디에 저장하는 것이 안전할까요?"
A: 플로우 각 단계별 역할과 토큰 저장소별 보안 고려사항(LocalStorage의 XSS 취약점, HttpOnly 쿠키의 CSRF 방어 이점 등)을 명확히 설명
React.lazy 등을 이용한 코드 스플리팅은 초기 로딩 성능을 개선하는 주된 목적 외에도, 오류의 영향 범위를 제한하는 부수적인 효과를 기대할 수 있습니다.
만약 특정 페이지나 기능에 해당하는 코드 청크 로딩에 실패하거나 해당 청크 내에서 런타임 오류가 발생하더라도, 애플리케이션 전체가 중단되는 대신 Suspense의 fallback이나 ErrorBoundary를 통해 해당 부분만 오류 처리를 할 수 있는 가능성이 생깁니다. (물론, 공통 모듈 오류는 여전히 전체에 영향을 줄 수 있습니다.)
npm audit, Snyk, Dependabot 같은 도구를 사용하여 알려진 보안 취약점이 있는 라이브러리를 확인하고 주기적으로 업데이트합니다. 불필요한 의존성은 제거합니다.
서드파티 스크립트 로딩 제어
:
HTML <script> 태그에 async 또는 defer 속성을 사용하여 페이지 렌더링을 막지 않도록 합니다.
정말 필요한 시점에만 스크립트를 동적으로 로드하는 방안을 고려합니다.
Subresource Integrity (SRI)
CDN 등 외부 서버에서 스크립트나 스타일시트를 로드할 때, 해당 리소스의 해시 값을 미리 <script> 나 <link> 태그의 integrity 속성에 명시하는 방법입니다.
브라우저는 리소스를 다운로드한 후 해시 값을 비교하여, 만약 파일 내용이 변경되었다면 로드를 차단합니다.
오늘은 React 애플리케이션의 보안과 안정성을 높이기 위한 중요한 개념과 방법들을 살펴보았습니다.
XSS
공격은 사용자 입력을 안전하게 처리(자동 이스케이핑, 입력 정제)하고 CSP를 설정하여 예방해야 합니다. dangerouslySetInnerHTML 사용은 극도로 주의해야 합니다.
CSRF
공격은 예측 불가능한 토큰(CSRF 토큰)을 사용하거나 SameSite 쿠키 속성을 활용하여 서버와 클라이언트가 함께 방어해야 합니다.
OAuth 2.0/OIDC
는 안전한 인증/인가를 위한 표준 프로토콜이며, 프론트엔드는 사용자를 인가 서버로 안내하고 토큰을 안전하게 관리하는 역할을 수행합니다. 특히 SPA에서는 PKCE를 포함한 인가 코드 플로우가 권장됩니다.
코드 스플리팅
은 성능 외에 오류 격리에도 일부 도움이 될 수 있으며,
서드파티 스크립트
는 편리하지만 성능, 보안, 안정성 측면의 위험을 내포하므로 신중한 관리와 감시가 필요합니다. SRI, CSP 등이 도움이 됩니다.
보안과 안정성은 한 번 설정하고 끝나는 것이 아니라, 지속적인 관심과 노력이 필요한 영역입니다.
새로운 취약점이 발견되고 기술이 변화함에 따라 우리의 방어 전략도 꾸준히 업데이트되어야 합니다.
무엇보다 중요한 것은 보안과 안정성을 개발 문화의 일부로 받아들이고, 설계 단계부터 배포, 운영에 이르기까지 모든 과정에서 항상 염두에 두는 자세일 것입니다.
다음 시간에는 React 개발 생산성과 코드 품질을 높여주는
개발자 도구와 테스트
방법에 대해 알아보겠습니다. React DevTools부터 테스트 라이브러리(Jest, React Testing Library), Storybook까지 다양한 도구들을 살펴보겠습니다.