
암호 해독 가이드 더 이상 두렵지 않은 자바스크립트 정규표현식
우리 앞의 암호문, 정규표현식
정규표현식(Regular Expression)은 많은 개발자에게 애증의 대상입니다.
문자열 검색, 치환, 유효성 검사 등 강력한 기능을 제공하지만, 그 대가로 마치 고대의 암호문 같은 복잡하고 난해한 문법을 요구하기 때문입니다.
한 줄의 정규표현식을 해독하기 위해 몇 시간을 끙끙 앓거나, 반대로 내가 작성한 코드를 몇 달 뒤에 스스로도 알아보지 못하는 경험은 누구에게나 있을 것입니다.
하지만 정규표현식은 정말 '읽기 어려운 것'으로만 남아야 할까요.
다행히도, 현대 자바스크립트는 정규표현식을 더 쉽고 명확하게 다룰 수 있는 여러 가지 강력한 기능들을 제공하고 있습니다.
이 글에서는 마치 암호문 같았던 정규표현식을 어떻게 하면 '사람이 읽을 수 있는 코드'로 바꿀 수 있는지, 그 구체적인 기법과 철학을 하나의 예제를 통해 단계적으로 탐구해 보고자 합니다.
해독 대상 오늘의 예제
우리의 여정은 하나의 간단한 정규표현식에서 시작합니다.
이 정규표현식은 API 시그니처 문자열에서 접두사(`new` 또는 `get`)와 이름 부분을 추출하는 역할을 합니다.
const RE_API_SIGNATURE =
/^(new |get )?([A-Za-z0-9_.\[\]]+)/;
지금 당장은 이 코드가 무엇을 하는지 한눈에 파악하기 어렵습니다.
하지만 이 글을 끝까지 따라오시면, 이 암호문이 얼마나 명확하고 읽기 쉬운 코드로 변신하는지 목격하게 될 것입니다.
첫 번째 단서 플래그와 이름표 달기
가장 먼저 우리가 할 일은 최신 기능을 사용하기 위한 준비와, 각 부분에 의미 있는 이름표를 달아주는 것입니다.
v 플래그 더 강력하고 예측 가능한 패턴
최신 자바스크립트 명세에 추가된 /v 플래그는 기존 정규표현식의 여러 단점을 보완하고 새로운 기능을 제공합니다.
기존 /u 플래그의 유니코드 지원 기능을 포함하면서, 문자 클래스(character class, [...]) 내에서 차집합이나 교집합 같은 '집합 연산'을 가능하게 해주는 등 훨씬 강력한 기능을 제공합니다.
지금 당장 우리 예제에 큰 변화를 주지는 않지만, 앞으로의 확장성과 안정성을 위해 /v 플래그를 사용하는 것은 좋은 습관입니다.
const RE_API_SIGNATURE =
/^(new |get )?([A-Za-z0-9_.\[\]]+)/v;
참고로, 여러 개의 플래그를 사용할 때는 알파벳 순서로 정렬하는 것이 좋습니다.
(예: /gvi 대신 /giv) 이는 코드의 일관성을 높여주고, 자바스크립트가 내부적으로 정규표현식을 문자열로 표현하는 방식과도 일치합니다.
이름 있는 캡처 그룹 의미를 부여하는 가장 쉬운 방법
기존 정규표현식은 두 개의 캡처 그룹 (...)을 가지고 있습니다.
우리는 매치 결과에서 이 그룹들을 match[1], match[2]와 같이 '마법의 숫자'로 접근해야 합니다.
이는 코드의 가독성을 해치고, 그룹의 순서가 바뀌면 버그를 유발하는 주된 원인이 됩니다.
'이름 있는 캡처 그룹 (?<name>...)'을 사용하면 이 문제를 해결할 수 있습니다.
const RE_API_SIGNATURE =
/^(?<prefix>new |get )?(?<name>[A-Za-z0-9_.\[\]]+)/v;
이제 우리는 match.groups.prefix, match.groups.name처럼 의미 있는 이름으로 각 그룹에 접근할 수 있습니다.
코드 자체가 스스로를 설명하는 '자기 문서화(self-documenting)'의 첫걸음입니다.
두 번째 단서 공백과 주석이라는 마법
지금까지의 개선에도 불구하고, 우리 정규표현식은 여전히 한 줄에 빽빽하게 뭉쳐 있어 읽기 어렵습니다.
만약 일반 코드처럼 줄 바꿈과 들여쓰기, 그리고 주석을 자유롭게 사용할 수 있다면 어떨까요.
'무의미한 공백(insignificant whitespace)' 기능이 바로 그것을 가능하게 합니다.
라이브러리를 활용한 접근법 Regex+
아쉽게도 자바스크립트의 기본 정규표현식 리터럴은 이 기능을 지원하지 않습니다.
(향후 /x 플래그로 표준화될 제안이 진행 중입니다.) 하지만 Regex+와 같은 라이브러리의 템플릿 태그를 사용하면 이 기능을 즉시 활용할 수 있습니다.
import { regex } from 'regex-plus';
const RE_API_SIGNATURE = regex`
^ # 문자열의 시작
(?<prefix> # (선택적) 접두사 그룹
new \x20 # "new " (생성자)
| # 또는
get \x20 # "get " (게터)
)?
(?<name> # 이름 그룹
# 심볼 키를 위해 대괄호([])가 필요합니다.
[
A-Z a-z 0-9 _ # 영문, 숫자, 밑줄
. # 점
\[ \] # 대괄호 (이스케이프 필요)
]+
)
`;
어떤가요.
이전과는 비교할 수 없을 정도로 읽기 쉬워졌습니다.
각 부분이 어떤 역할을 하는지, 왜 특정 문자가 필요한지 주석을 통해 명확하게 설명할 수 있습니다.
여기서 주목할 점은, 모든 공백이 무시되기 때문에 실제 '공백 문자'를 표현하기 위해서는 \x20(스페이스의 16진수 코드) 같은 이스케이프 시퀀스를 사용해야 한다는 것입니다.
라이브러리 없이 구현하는 방법
라이브러리 추가가 부담스럽다면, 자바스크립트의 기본 기능만으로도 비슷한 효과를 낼 수 있습니다.
템플릿 리터럴과 replaceAll() 메서드를 조합하는 트릭입니다.
const RE_API_SIGNATURE_STRING = `
^
(?<prefix>
new \\x20
|
get \\x20
)?
(?<name>
[
A-Z a-z 0-9 _
.
\\[ \\]
]+
)
`.replaceAll(/\s+/g, ''); // 모든 공백 문자를 제거
const RE_API_SIGNATURE = new RegExp(RE_API_SIGNATURE_STRING, 'v');
이 방법은 String.raw 대신 일반 템플릿 리터럴을 사용하기 때문에 백슬래시를 한 번 더 이스케이프(\\)해야 하는 번거로움이 있지만, 외부 의존성 없이 가독성을 높일 수 있는 좋은 대안입니다.
배열과 join을 이용한 가장 실용적인 접근법
사실 많은 숙련된 개발자들은 라이브러리나 복잡한 트릭 대신, 배열과 join 메서드를 사용하는 방식을 선호합니다.
정규표현식의 각 논리적인 부분을 문자열 배열의 요소로 나누어 작성한 뒤, 마지막에 합치는 방식입니다.
const RE_API_SIGNATURE = new RegExp([
'^',
'(?<prefix>new |get )?',
'(?<name>[A-Za-z0-9_.\\[\\]]+)'
].join(''), 'v');
// 주석을 추가하여 가독성을 더욱 높일 수 있습니다.
const RE_API_SIGNATURE_WITH_COMMENTS = new RegExp([
'^', // 문자열의 시작
'(?<prefix>new |get )?', // 접두사 그룹 (new 또는 get)
'(?<name>', // 이름 그룹 시작
'[A-Za-z0-9_.\\[\\]]+', // 허용되는 문자들
')' // 이름 그룹 끝
].join(''), 'v');
이 방법은 외부 라이브러리도 필요 없고, replaceAll 트릭보다 훨씬 직관적이며, 주석을 통해 각 부분의 의도를 명확히 설명할 수 있어 유지보수 측면에서 매우 유리합니다.
복잡한 정규표현식을 다룰 때 가장 먼저 고려해 볼 만한 실용적인 패턴입니다.
세 번째 단서 테스트로 확신 더하기
아무리 읽기 쉽게 만들어도, 정규표현식이 의도대로 정확하게 동작하는지는 '테스트'를 통해 반드시 검증해야 합니다.
정규표현식은 잠재적인 엣지 케이스가 많기 때문에, 테스트 코드는 단순한 검증을 넘어 살아있는 '문서'의 역할을 합니다.
`Jest`나 `Vitest`와 같은 테스팅 프레임워크를 사용하면 이 과정을 체계적으로 관리할 수 있습니다.
// RE_API_SIGNATURE.test.js
import { describe, it, expect } from 'vitest';
// 테스트 대상 정규표현식
const RE_API_SIGNATURE = /.../;
function getCaptures(signature) {
const match = RE_API_SIGNATURE.exec(signature);
return match ? { ...match.groups } : null;
}
describe('RE_API_SIGNATURE', () => {
it('게터(getter) 시그니처를 올바르게 파싱해야 합니다', () => {
const result = getCaptures('get Map.prototype.size');
expect(result).toEqual({
prefix: 'get ',
name: 'Map.prototype.size',
});
});
it('생성자(new) 시그니처를 올바르게 파싱해야 합니다', () => {
const result = getCaptures('new Array(len = 0)');
expect(result).toEqual({
prefix: 'new ',
name: 'Array',
});
});
it('접두사가 없는 경우를 올바르게 처리해야 합니다', () => {
const result = getCaptures('Array.prototype.push(...items)');
expect(result).toEqual({
prefix: undefined,
name: 'Array.prototype.push',
});
});
it('심볼 키를 포함하는 경우를 올바르게 파싱해야 합니다', () => {
const result = getCaptures('Map.prototype[Symbol.iterator]()');
expect(result).toEqual({
prefix: undefined,
name: 'Map.prototype[Symbol.iterator]',
});
});
});
이렇게 테스트 케이스를 작성해 두면, 미래에 누군가가 정규표현식을 수정하더라도 의도치 않은 버그가 발생하는 것을 막아주는 든든한 안전망이 됩니다.
정규표현식은 기계를 위한 언어가 아닙니다
많은 개발자들이 정규표현식을 어려워하는 이유는 그것을 '읽기 어려운 것'으로만 생각하기 때문입니다.
하지만 우리가 살펴본 여러 기법들을 활용하면, 정규표현식도 일반 코드처럼 명확한 구조와 주석을 가진, '사람을 위한 코드'로 작성할 수 있습니다.
만약 우리가 모든 자바스크립트 코드를 공백과 주석 없이 한 줄로 작성해야 한다고 상상해 보십시오.
아마 끔찍할 겁니다.
정규표현식도 마찬가지입니다.
이제부터는 단순히 동작하는 정규표현식이 아니라, '누가 읽어도 이해할 수 있고, 미래의 내가 봐도 쉽게 수정할 수 있는' 정규표현식을 작성하는 것을 목표로 삼아보는 것은 어떨까요.
그것이 바로 암호 해독의 열쇠이자, 진정한 의미의 프로페셔널한 코드 작성법일 것입니다.
'Javascript' 카테고리의 다른 글
| Next.js는 어떻게 React Compiler를 돌리는가 SWC와 Babel의 현명한 공존 (2) | 2025.08.24 |
|---|---|
| ECMAScript 2025 최종 승인 무엇이 달라졌나? (0) | 2025.07.13 |
| 더 안전한 타입스크립트 Map과 배열 다루기 고급 패턴 탐구 (0) | 2025.07.13 |
| 자바스크립트의 배신 타입스크립트는 Iterator 이름 충돌을 어떻게 해결했나 (0) | 2025.07.13 |
| 2025년 최고의 자바스크립트(JavaScript) 대안, 리스크립트(ReScript) 파헤치기! (1) | 2025.05.20 |