| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- APK
- 라이렌
- html
- phpDocumentor
- php
- 설정
- 헬퍼
- rairen
- jw player
- Database
- 코드이그나이터
- json
- 옵션표
- codeigniter3
- 웹 프로그래밍
- 함수
- CI3
- API
- javascript
- CodeIgniter
- mysql
- FCM
- 그누보드
- 후크
- config
- 영카트
- 웹개발
- jquery
- function
- 안드로이드
- Today
- Total
프로그램 개발서
JavaScript RegExp.escape()로 동적 정규식 안전하게 만들기: 점·하이픈·역참조 처리 본문

문제와 사용 상황
검색창에 입력한 문자열을 정규식으로 찾아 강조하거나, 사용자가 고른 파일명을 필터링할 때 new RegExp(input)처럼 동적 정규식을 만들기 쉽습니다. 문제는 입력값이 평범한 문자열이 아니라 정규식 문법으로 해석된다는 점입니다.
예를 들어 사용자가 a.b를 입력했다고 가정해 보겠습니다. 점(.)은 정규식에서 “줄바꿈을 제외한 임의의 한 문자”라는 뜻입니다. 따라서 아래 코드는 a.b뿐 아니라 aXb도 일치시킵니다.
const input = "a.b";
const pattern = new RegExp(`^${input}$`);
console.log(pattern.test("a.b")); // true
console.log(pattern.test("aXb")); // true: 의도하지 않은 일치
괄호, 대괄호, *, +, ?, |, ^, $도 같은 문제를 만듭니다. 입력값을 검색어처럼 문자 그대로 취급해야 한다면 정규식 문법을 제거해야 합니다. 최신 JavaScript에는 이 용도의 표준 메서드 RegExp.escape()가 있습니다.
이 글에서 “안전하다”는 표현은 입력 문자열이 정규식 문법으로 주입되지 않고 리터럴 패턴 조각으로 처리된다는 뜻입니다. HTML, SQL, 셸 명령을 이스케이프한다는 뜻은 아닙니다.
테스트 환경·버전·날짜
- 확인 시각: 2026-08-01T03:30:09+09:00
- Node.js: v24.14.0
- 브라우저: Google Chrome 150.0.0.0
- 운영체제: Windows 10 계열 사용자 에이전트
- 공식 문서 확인 날짜: 2026-08-01
Node.js와 Chrome에서는 typeof RegExp.escape === "function"이 true였고 같은 다섯 가지 테스트를 모두 통과했습니다. Firefox, Safari, 구형 브라우저와 오래된 Node.js 버전은 이번 실행에서 테스트하지 않았습니다.
MDN은 RegExp.escape()를 Baseline 2025 기능으로 표시하며 2025년 5월 이후 최신 브라우저에서 널리 사용할 수 있다고 안내합니다. 하지만 “최신 브라우저” 범위 밖의 사내 PC, 오래된 WebView, 장기 지원 런타임까지 자동으로 포함되는 것은 아닙니다.
최소 재현 코드
실패하는 코드와 수정한 코드를 나란히 실행해 보면 차이가 분명합니다.
const input = "a.b";
const unsafePattern = new RegExp(`^${input}$`);
const safePattern = new RegExp(`^${RegExp.escape(input)}$`);
console.log(RegExp.escape(input)); // \x61\.b
console.log(unsafePattern.test("aXb")); // true
console.log(safePattern.test("a.b")); // true
console.log(safePattern.test("aXb")); // false
출력의 \x61은 문자 a를 16진수 이스케이프로 표현한 것입니다. 처음 보면 a\.b면 충분하지 않나 싶지만, 첫 문자를 이렇게 처리하는 데는 이유가 있습니다. 이 조각이 \1, \x0, \u000, \c 같은 앞선 이스케이프 바로 뒤에 붙어도 하나의 더 긴 이스케이프로 합쳐지지 않게 하기 위해서입니다.
테스트 케이스와 결과표

| 테스트 | 입력 또는 조건 | 실제 결과 | 판정 |
| 점을 문자 그대로 검색 | a.b |
\x61\.b, aXb는 불일치 |
PASS |
| 연산자 문자열 검색 | (a+)+$ |
\(a\+\)\+\$, 원문만 일치 |
PASS |
| Unicode 모드의 하이픈 | foo-bar |
\x66oo\x2dbar, 생성·검색 성공 |
PASS |
| 역참조 뒤 숫자 | (a)\1 뒤에 1 결합 |
(a)\1\x31, aa1 일치 |
PASS |
| 문자열이 아닌 입력 | 123 |
TypeError |
PASS |
Node.js와 Chrome에서 같은 결과를 얻었습니다. 첫 후보가 통과했기 때문에 다른 후보를 추가 실행해 결과를 늘리지 않았습니다.
단순 replace 함수와 다른 부분
인터넷에는 다음 형태의 보조 함수가 많이 남아 있습니다.
function escapeRegExpOld(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
이 함수는 흔한 문법 문자 일부에는 동작하지만 RegExp.escape()와 동등하지 않습니다. 표준 메서드는 다음 조건까지 고려합니다.
- 첫 ASCII 영문자·숫자를
\xHH로 바꿔 앞선 역참조·문자 이스케이프와 합쳐지는 일을 막습니다. -,,,=,<,>,#,&,!,%,:,;,@,~, 따옴표 같은 구두점은 문맥에 따라\xHH로 처리합니다.- 줄바꿈, 탭, 공백, 비 ASCII 공백과 줄 구분 문자를 올바른 이스케이프로 바꿉니다.
- 고립 서로게이트를
\uXXXX로 바꿔 유효한 패턴 조각을 만듭니다. - 문자열이 아닌 입력을 암묵적으로 문자열화하지 않고
TypeError를 던집니다.
구형 환경 지원이 필요하면 단순 helper를 표준 메서드와 같다고 가정하지 말고, 호환성 표를 확인한 뒤 검증된 polyfill을 프로젝트 테스트에 포함하는 편이 안전합니다.
최종 코드
입력값 전체를 검색어로 취급하는 함수는 기능 확인과 타입 확인을 함께 두면 실패 원인이 명확해집니다.
function createLiteralRegExp(input, options = {}) {
const { flags = "giu", whole = false } = options;
if (typeof input !== "string") {
throw new TypeError("검색어는 문자열이어야 합니다.");
}
if (typeof RegExp.escape !== "function") {
throw new Error(
"이 환경은 RegExp.escape()를 지원하지 않습니다. " +
"검증된 polyfill을 먼저 적용하세요.",
);
}
const literal = RegExp.escape(input);
const source = whole ? `^(?:${literal})$` : literal;
return new RegExp(source, flags);
}
const contains = createLiteralRegExp("a.b", { flags: "iu" });
console.log(contains.test("파일명: a.b")); // true
console.log(contains.test("파일명: aXb")); // false
const exact = createLiteralRegExp("foo-bar", {
flags: "u",
whole: true,
});
console.log(exact.test("foo-bar")); // true
console.log(exact.test("prefix foo-bar")); // false
사용자가 정규식 자체를 작성해야 하는 고급 검색 기능이라면 RegExp.escape()를 적용하면 안 됩니다. 이 메서드는 입력을 “정규식 문법”이 아니라 “문자열 그대로” 해석해야 할 때 사용합니다. 두 모드를 UI에서 명확히 나누는 것이 좋습니다.
실패 조건·브라우저·버전 차이
RegExp.escape is not a function
런타임이 이 메서드를 구현하지 않은 경우입니다. 배포 대상 브라우저와 WebView, Node.js 버전을 먼저 확인하고 검증된 polyfill을 번들에 포함해야 합니다. 지원하지 않는 환경에서 결과 문자열을 추측해 작성하면 안 됩니다.
숫자나 객체를 넘긴 경우
RegExp.escape(123)은 "123"을 반환하지 않고 TypeError를 던집니다. 숫자를 검색하려는 의도라면 호출부에서 RegExp.escape(String(value))처럼 변환 의도를 코드로 드러내세요.
정규식 문법을 허용하려던 경우
입력 ^foo.*bar$를 고급 패턴으로 쓰고 싶었다면 RegExp.escape()를 거친 뒤에는 문자 그대로 ^foo.*bar$만 찾습니다. 일반 검색과 정규식 검색을 하나의 입력창에서 묵시적으로 섞지 마세요.
g·y 플래그의 상태
전역 또는 sticky 정규식의 test()를 반복 호출하면 lastIndex가 바뀝니다. RegExp.escape()와 별개의 동작입니다. 반복 검사라면 호출마다 새 정규식을 만들거나 lastIndex = 0을 명시하고, 다중 결과가 필요하면 matchAll() 같은 API를 고려하세요.
보안·호환성 주의사항
RegExp.escape()는 입력값 내부의 *, +, 괄호 같은 연산자를 리터럴로 바꾸므로 사용자가 검색어로 정규식 구조를 주입하는 문제를 줄입니다. 그러나 함수 바깥에서 작성한 패턴 자체가 (.*)+처럼 비효율적이면 ReDoS 가능성은 그대로 남습니다.
사용자 입력 길이도 제한하는 편이 좋습니다. 수 MB 문자열을 그대로 패턴으로 만들면 이스케이프에 성공해도 컴파일·검색 비용과 메모리 사용량이 커질 수 있습니다. 화면 검색이라면 제품 요구사항에 맞는 최대 길이를 정하고 초과 입력을 거절하세요.
이 결과를 HTML, SQL, URL, 셸 명령 이스케이프에 재사용해서는 안 됩니다. 각 문맥은 별도의 안전한 API가 필요합니다. 또한 브라우저 호환성은 기능 감지로 확인하고, 지원하지 않는 환경의 대응은 “검증된 polyfill”, “일반 문자열 검색 API 사용”, “기능 비활성화” 중 하나로 명시하세요.
직접 확인 방법
- Node.js에서
node --version을 실행해 테스트 버전을 기록합니다. - 아래 한 줄로 기능 지원 여부를 확인합니다.
console.log(typeof RegExp.escape === "function");
- 이 패키지의 테스트 폴더에서 다음 명령을 실행합니다.
node .\work\tistory-automation\tests\2026-08-01-js-regexp-escape\test.mjs
- 마지막 줄이
overall=PASS인지 확인합니다. - 브라우저에서는 같은 테스트 폴더의
index.html을 로컬 HTTP 서버로 열고 화면 마지막 줄이overall=PASS인지 확인합니다.file://직접 열기보다 테스트 폴더의server.mjs를 사용하면 실행 조건을 일정하게 유지할 수 있습니다.
이번 실행의 전체 실제 출력은 work/tistory-automation/tests/2026-08-01-js-regexp-escape/actual-output.txt에 저장했습니다.
공식 참고자료
두 문서는 2026-08-01에 직접 확인했습니다. 커뮤니티 질문은 검색 의도 참고에만 사용했고, 동작과 예외 조건은 공식 문서와 독립 테스트로 확인했습니다.
변경 이력
- 2026-08-01: Node.js v24.14.0과 Chrome 150.0.0.0에서 다섯 가지 사례를 테스트하고 최초 작성했습니다.
'Javascript' 카테고리의 다른 글
| JavaScript Error.isError()로 다른 실행 환경의 오류 객체 판별하기 (0) | 2026.08.07 |
|---|---|
| JavaScript Array.fromAsync()로 비동기 반복값을 순서대로 배열로 만들기 (0) | 2026.08.07 |
| JavaScript URLSearchParams 중복 키 처리: getAll(), append(), set() 차이 (0) | 2026.07.28 |
| JavaScript JSON.parse에서 큰 정수 정밀도 손실 막기: context.source와 BigInt (0) | 2026.07.27 |
| 그누보드5 이윰빌더 시즌4 CHEditor5에 에드온 이모티콘 삽입 (0) | 2024.06.24 |