Wednesday, November 6, 2019

실무에서 바로 쓰는 자바스크립트 정규표현식 완벽 분석

자바스크립트 개발자라면 누구나 문자열이라는 거대한 바다를 항해해야 합니다. 사용자 입력 폼의 유효성을 검사하고, 복잡한 로그 파일에서 의미 있는 데이터를 추출하며, 특정 형식의 텍스트를 동적으로 변환하는 등, 문자열 처리는 프로젝트의 성패를 좌우하는 핵심 기술입니다. 이 광활한 바다에서 가장 강력한 나침반이자 만능 도구가 되어주는 것이 바로 **정규표현식(Regular Expression, 이하 Regex)**입니다.

하지만 많은 개발자들이 Regex의 첫인상에 압도당합니다. 마치 고대 상형문자처럼 보이는 기호들의 나열은 시작도 하기 전에 포기하게 만드는 높은 장벽처럼 느껴집니다. '일단 구글에서 복사해서 붙여넣고 보자'는 생각으로 임시방편적인 해결에 그치는 경우도 부지기수입니다. 이 글은 더 이상 Regex를 두려움의 대상으로 남겨두고 싶지 않은 모든 개발자를 위한 최종 안내서입니다. Regex가 무엇인지 근본적인 개념부터 시작해, 자바스크립트에서 이를 자유자재로 다루는 방법, 그리고 까다로운 실무 문제들을 해결하는 고급 기법과 실전 예제까지, 모든 것을 깊이 있게 파헤쳐 보겠습니다. 이 글을 끝까지 정독하고 나면, 당신은 더 이상 암호를 해독하는 고고학자가 아니라, 문자열의 패턴을 창조하고 지배하는 마법사가 될 것입니다.

1. 정규표현식과의 첫 만남: 개념 바로 세우기

정규표현식이란 무엇일까요? 가장 단순하게 정의하면 **'문자열의 특정 패턴을 기술하는 형식 언어(Formal Language)'**입니다. 파일 탐색기에서 `*.jpg`를 입력해 모든 JPEG 이미지를 찾는 것과 원리는 비슷하지만, 그 표현력과 정교함은 차원을 달리합니다. Regex를 사용하면 다음과 같은 복잡한 규칙들을 단 한 줄의 패턴으로 정의할 수 있습니다.

  • "대한민국 휴대폰 번호 형식(010-XXXX-XXXX 또는 010XXXXXXXX)에 맞는 문자열 찾기"
  • "HTML 문자열에서 태그만 찾아내고, 그 안의 src 속성 값 추출하기"
  • "최소 8자 이상이며, 대문자, 소문자, 숫자를 반드시 하나 이상 포함하는 비밀번호 형식 검증하기"

이처럼 Regex는 단순 텍스트 검색을 넘어, 문자열의 '구조'와 '규칙'을 찾아내는 강력한 도구입니다. JavaScript뿐만 아니라 Python, Java, C#, Ruby, PHP, 심지어 데이터베이스(Oracle, MySQL)나 텍스트 에디터(VS Code, Sublime Text)에 이르기까지 IT 기술 전반에서 표준처럼 사용됩니다. 따라서 한번 제대로 익혀두면 당신의 개발 생산성과 문제 해결 능력을 비약적으로 향상시키는 평생의 자산이 될 것입니다.

2. JavaScript에서 Regex 생성하기: 두 가지 핵심 방법론

자바스크립트에서는 정규표현식 객체를 생성하는 두 가지 방법을 제공합니다. 바로 '리터럴(Literal)' 방식과 '생성자(Constructor)' 방식입니다. 이 둘의 차이점을 명확히 이해하는 것이 Regex 활용의 첫걸음입니다.

2.1. 정규표현식 리터럴 (Regular Expression Literal)

가장 널리 쓰이고 직관적인 방법입니다. 찾고 싶은 패턴을 슬래시(/) 기호로 감싸서 직접 작성합니다. 이 방식의 가장 큰 특징은 **스크립트가 로드되는 시점에 컴파일된다**는 점입니다. 따라서 패턴이 고정되어 있는 경우, 반복적으로 사용될 때 성능상 이점을 가집니다.

const regex = /pattern/flags;
  • /pattern/: 이 부분이 실제 검색 패턴입니다. 예를 들어, `javascript`라는 단어를 찾는다면 /javascript/가 됩니다.
  • flags (선택 사항): 검색의 동작을 제어하는 옵션(플래그)입니다. 여러 개를 동시에 사용할 수 있습니다.
    • g (Global): 문자열 전체에서 일치하는 모든 패턴을 찾습니다. 이 플래그가 없으면 첫 번째 일치 항목만 찾고 검색을 종료합니다.
    • i (Ignore case): 대소문자를 무시하고 검색합니다. /apple/i는 "apple", "Apple", "APPLE" 모두와 일치합니다.
    • m (Multi-line): 여러 줄(multi-line) 모드를 활성화합니다. 이 플래그가 있으면, 문자열의 시작(^)과 끝($)을 찾는 메타문자가 각 줄(line)의 시작과 끝에 대응하게 됩니다.
    • s (dotAll): 메타문자 .이 개행 문자(\n)까지 포함한 모든 문자와 일치하도록 만듭니다. 기본적으로 .은 개행 문자와 일치하지 않습니다.
    • u (Unicode): 유니코드 문자를 올바르게 처리합니다. 이모지(emoji)나 다양한 언어의 문자를 다룰 때 필수적입니다.
    • y (Sticky): `g` 플래그와 유사하지만, 특정 위치에서부터의 검색만 수행합니다. Regex 객체의 `lastIndex` 속성과 함께 사용됩니다.

예시:

const text = "JavaScript is a powerful language. I love javascript!";
// 'javascript'라는 단어를 대소문자 구분 없이, 문자열 전체에서 찾기
const regex = /javascript/gi;

const matches = text.match(regex);
console.log(matches); // ["JavaScript", "javascript"]

2.2. RegExp 생성자 (RegExp Constructor)

new RegExp() 생성자를 호출하여 정규표현식 객체를 만듭니다. 이 방법의 핵심 용도는 **패턴이 동적으로 변경되어야 할 때**입니다. 예를 들어, 사용자 입력값이나 변수에 담긴 문자열을 Regex 패턴으로 사용하고 싶을 때 유용합니다.

const regex = new RegExp('pattern', 'flags');

리터럴 방식과 달리, 패턴과 플래그를 모두 문자열 형태로 전달합니다. 여기서 초보자들이 가장 많이 저지르는 실수가 발생합니다. 패턴을 일반 문자열로 작성하기 때문에, Regex에서 특별한 의미를 가지는 메타문자 중 백슬래시(\)를 사용하는 경우, 자바스크립트 문자열 규칙에 따라 한 번 더 이스케이프(escape) 처리를 해주어야 합니다. 즉, '모든 숫자'를 의미하는 \d를 패턴으로 사용하려면 문자열 내에서는 '\\d'라고 작성해야 합니다.

예시:

const userInput = "apple"; // 사용자가 검색어로 'apple'을 입력했다고 가정
const flags = "gi";

// 변수에 담긴 값을 패턴으로 사용
const dynamicRegex = new RegExp(userInput, flags);

const text = "An Apple a day keeps the doctor away. apple pie is delicious.";
console.log(text.match(dynamicRegex)); // ["Apple", "apple"]

// 백슬래시 이스케이프 예제
const textWithNumbers = "My age is 30.";
// const digitRegex = new RegExp('\d+', 'g'); // 잘못된 사용! '\d'는 문자열에서 이스케이프 시퀀스로 인식되지 않을 수 있음
const digitRegex = new RegExp('\\d+', 'g'); // 올바른 사용! '\\'가 문자열 내에서 '\' 문자를 의미하게 됨

console.log(textWithNumbers.match(digitRegex)); // ["30"]

가장 흔한 함정: 리터럴과 문자열의 혼동

자바스크립트의 문자열 메서드(.match(), .replace() 등)는 인자로 Regex 객체뿐만 아니라 일반 문자열도 받을 수 있습니다. 이때, Regex 리터럴처럼 생긴 문자열을 전달하면 비극이 시작됩니다.

const myString = "There are 100 ways to code.";

// ❌ 잘못된 사용: '/[0-9]+/'는 정규표현식이 아니라, 슬래시와 대괄호, 숫자 등으로 이루어진 '일반 문자열'입니다.
// 자바스크립트는 myString에서 '/[0-9]+/'라는 텍스트를 글자 그대로 찾으려고 시도합니다.
console.log(myString.match('/[0-9]+/')); // null

// ✅ 올바른 사용 1: 정규표현식 리터럴 사용
// 따옴표 없이 슬래시로 감싸서 자바스크립트 엔진이 이를 Regex 객체로 인식하게 합니다.
console.log(myString.match(/[0-9]+/)); 
// ["100", index: 10, input: "There are 100 ways to code.", groups: undefined]

// ✅ 올바른 사용 2: RegExp 생성자 사용
// 패턴을 문자열 형태로 전달합니다.
const regex = new RegExp('[0-9]+');
console.log(myString.match(regex));
// ["100", index: 10, input: "There are 100 ways to code.", groups: undefined]

결론적으로, 패턴이 고정되어 있다면 가독성과 성능 면에서 리터럴(/[0-9]+/)을, 패턴이 변수처럼 동적으로 결정되어야 한다면 생성자(new RegExp())를 사용하는 것이 황금률입니다.

3. Regex 암호 해독: 핵심 문법 완전 정복

Regex의 진정한 힘은 다양한 메타문자와 기호들을 조합하여 정교한 패턴을 만드는 데서 나옵니다. 이 '암호'들을 하나씩 해독해 봅시다.

3.1. 기본 구성 요소: 앵커와 문자 클래스

  • . (마침표): '임의의 한 글자'를 의미합니다. 단, 개행 문자(\n)는 제외됩니다. (s 플래그 사용 시 포함)
    • /h.t/는 "hat", "hot", "h1t" 등과 일치하지만 "ht"나 "heat"와는 일치하지 않습니다.
  • ^ (캐럿): 문자열의 **시작**을 의미하는 앵커(anchor)입니다.
    • /^Hello/는 "Hello world"와 일치하지만, "world, Hello"와는 일치하지 않습니다.
  • $ (달러 기호): 문자열의 **끝**을 의미하는 앵커입니다.
    • /world$/는 "Hello world"와 일치하지만, "world is beautiful"과는 일치하지 않습니다.
    • /^apple$/는 오직 "apple"이라는 문자열과 정확히 일치합니다.
  • \d: 숫자(Digit) 한 개와 일치합니다. [0-9]와 동일합니다.
  • \D: 숫자가 아닌 문자 한 개와 일치합니다. [^0-9]와 동일합니다.
  • \w: 영문자, 숫자, 언더스코어(_)를 포함하는 '단어(Word) 문자' 한 개와 일치합니다. [A-Za-z0-9_]와 동일합니다.
  • \W: 단어 문자가 아닌 문자(공백, 특수문자 등) 한 개와 일치합니다. [^A-Za-z0-9_]와 동일합니다.
  • \s: 공백, 탭, 개행 문자 등 모든 공백(Space) 문자 한 개와 일치합니다.
  • \S: 공백 문자가 아닌 문자 한 개와 일치합니다.
  • \b: 단어의 경계(Word Boundary)를 의미합니다. 단어의 시작이나 끝 부분, 즉 단어 문자와 비단어 문자 사이의 위치와 일치합니다.
    • /\bcat\b/는 "The cat sat"의 'cat'과는 일치하지만, "catch"나 "tomcat"의 'cat'과는 일치하지 않습니다. 매우 유용합니다.
  • \B: 단어의 경계가 아닌 위치와 일치합니다.

3.2. 반복의 미학: 수량자(Quantifiers)

수량자는 바로 앞의 문자나 그룹이 몇 번 반복되는지를 지정합니다.

  • *: 바로 앞의 패턴이 0번 이상 반복되는 경우와 일치합니다. (없거나, 많거나)
    • /go*gle/는 "ggle", "google", "goooogle" 등과 일치합니다.
  • +: 바로 앞의 패턴이 1번 이상 반복되는 경우와 일치합니다. (최소 한 번은 등장해야 함)
    • /go+gle/는 "google", "goooogle" 등과 일치하지만 "ggle"과는 일치하지 않습니다.
  • ?: 바로 앞의 패턴이 0번 또는 1번 나타나는 경우와 일치합니다. (있거나 없거나)
    • /colou?r/는 "color"와 "colour" 모두와 일치합니다.
  • {n}: 바로 앞의 패턴이 정확히 n 반복되는 경우와 일치합니다.
    • /\d{3}/는 "123"과 일치하지만 "12"나 "1234"의 일부만 일치합니다.
  • {n,}: 바로 앞의 패턴이 최소 n번 이상 반복되는 경우와 일치합니다.
    • /\d{2,}/는 "12", "123", "12345" 등과 일치합니다.
  • {n,m}: 바로 앞의 패턴이 최소 n번, 최대 m 반복되는 경우와 일치합니다.
    • /^\d{3,5}$/는 3자리, 4자리, 5자리 숫자로만 이루어진 문자열과 일치합니다.

탐욕스러운(Greedy) vs 게으른(Lazy) 매칭

기본적으로 수량자(*, +, {})는 '탐욕스럽게(Greedy)' 동작합니다. 이는 가능한 한 가장 긴 문자열을 찾으려고 시도하는 것을 의미합니다.

const text = "

first

and

second

"; const greedyRegex = /<p>.*<\/p>/; console.log(text.match(greedyRegex)[0]); // 결과: "<p>first</p> and <p>second</p>" // `.*`가 첫 번째 `

`부터 마지막 `

`까지 모든 것을 삼켜버렸습니다.

이런 동작을 원하지 않을 경우, 수량자 뒤에 물음표(?)를 붙여 '게으르게(Lazy)' 만들 수 있습니다. 게으른 수량자는 가능한 한 가장 짧은 문자열을 찾으려고 시도합니다.

const text = "

first

and

second

"; const lazyRegex = /<p>.*?<\/p>/; console.log(text.match(lazyRegex)[0]); // 결과: "<p>first</p>" // `.*?`가 첫 번째 `</p>`를 만나자마자 매칭을 멈췄습니다.

3.3. 구조화와 선택: 그룹과 범위

  • [...] (문자 집합): 대괄호 안의 어떤 문자든 '하나'와 일치합니다.
    • /[abc]/는 'a', 'b', 'c' 중 한 글자와 일치합니다.
    • 하이픈(-)으로 범위를 지정할 수 있습니다: [a-z], [0-9], [A-Z].
    • /[a-zA-Z0-9]/는 영문 대소문자와 숫자 중 한 글자와 일치합니다. (\w와 비슷하지만 _는 제외)
  • [^...] (부정 문자 집합): 대괄호 안의 캐럿(^)은 '부정(NOT)'을 의미합니다. 괄호 안에 없는 모든 문자와 일치합니다.
    • /[^0-9]/는 숫자가 아닌 모든 문자와 일치합니다. (\D와 동일)
  • (...) (캡처링 그룹): 괄호는 여러 문자를 하나의 단위로 묶어줍니다. 수량자의 영향을 받게 하거나, 검색 결과에서 이 부분만 따로 추출(캡처)할 수 있습니다.
    • /(ha)+/는 "ha", "haha", "hahaha" 등과 일치합니다.
    • 캡처된 그룹은 나중에 $1, $2와 같은 형태로 재참조할 수 있습니다.
  • (?:...) (비캡처링 그룹): 그룹으로 묶어주지만, 캡처하지는 않습니다. 단순히 패턴의 우선순위를 지정하거나 그룹화를 할 목적일 때 사용합니다. 캡처링에 드는 약간의 메모리 오버헤드를 줄일 수 있습니다.
  • | (OR 연산자): 여러 패턴 중 하나를 선택합니다. '또는'의 의미입니다.
    • /cat|dog/는 "cat" 또는 "dog"와 일치합니다.
    • /I love (apple|banana)/는 "I love apple" 또는 "I love banana"와 일치합니다.

3.4. 고급 기술: 주변을 둘러보는 룩어라운드(Lookaround)

룩어라운드는 특정 패턴이 일치하는지를 '확인'만 하고, 실제 결과에는 포함시키지 않는 강력한 기능입니다. 'Zero-width assertion'이라고도 불리며, 조건은 만족해야 하지만 결과값에서는 빼고 싶을 때 사용합니다.

  • 긍정형 전방 탐색 (Positive Lookahead) (?=...): ... 패턴이 뒤따라오는 경우에만 앞의 패턴과 일치합니다.
    • 비밀번호 검증에 유용합니다: /^(?=.*\d)(?=.*[a-z]).{8,}$/는 '숫자가 포함'되고 '소문자가 포함'된 8자리 이상의 문자열과 일치합니다. 여기서 (?=.*\d) 부분은 실제 문자열을 소비하지 않고 조건만 확인합니다.
  • 부정형 전방 탐색 (Negative Lookahead) (?!...): ... 패턴이 뒤따라오지 않는 경우에만 앞의 패턴과 일치합니다.
    • /q(?!u)/는 'q' 다음에 'u'가 오지 않는 'q'와 일치합니다. "Iraq"의 'q'와는 일치하지만 "queen"의 'q'와는 일치하지 않습니다.
  • 긍정형 후방 탐색 (Positive Lookbehind) (?<=...): ... 패턴이 앞에 있는 경우에만 뒤의 패턴과 일치합니다.
    • /(?<=\$)\d+/는 달러 기호($) 바로 뒤에 오는 숫자들과 일치합니다. "Price: $100"에서 "100"을 찾아내지만, "$"는 결과에 포함하지 않습니다.
  • 부정형 후방 탐색 (Negative Lookbehind) (?: ... 패턴이 앞에 오지 않는 경우에만 뒤의 패턴과 일치합니다.
    • /(?는 달러 기호가 앞에 없는 숫자들과 일치합니다.

4. JavaScript 메서드와 Regex의 협주곡

이제 잘 만들어진 Regex 패턴을 자바스크립트의 내장 메서드들과 함께 사용하여 실제 작업을 수행해 봅시다.

4.1. `RegExp.prototype.test()`: 존재 유무 확인

가장 단순합니다. 문자열이 정규표현식과 일치하는지 여부를 `true` 또는 `false`로 반환합니다. 유효성 검사에 완벽합니다.

const isKoreanPhoneNumber = /^(010|011|016|017|018|019)-\d{3,4}-\d{4}$/;
console.log(isKoreanPhoneNumber.test("010-1234-5678")); // true
console.log(isKoreanPhoneNumber.test("01012345678"));   // false (하이픈 없음)
console.log(isKoreanPhoneNumber.test("02-123-4567"));    // false (01X가 아님)

4.2. `RegExp.prototype.exec()`: 상세한 정보와 상태를 가진 탐색

String.prototype.match()와 비슷하지만, `g` 플래그와 함께 사용할 때 상태를 유지한다는 중요한 차이점이 있습니다. `exec()`를 반복 호출하면, `lastIndex` 속성을 갱신하며 다음 일치 항목을 계속해서 찾아냅니다. 일치 항목을 찾으면 상세 정보가 담긴 배열을, 더 이상 없으면 `null`을 반환합니다.

const text = "Color: #FF0000, Background: #00FF00, Border: #0000FF";
const hexCodeRegex = /#([A-Fa-f0-9]{6})\b/g; // g 플래그 필수!

let match;
while ((match = hexCodeRegex.exec(text)) !== null) {
  console.log(`Found: ${match[0]}, Color code: ${match[1]}, Next search starts at: ${hexCodeRegex.lastIndex}`);
}
// 출력:
// Found: #FF0000, Color code: FF0000, Next search starts at: 15
// Found: #00FF00, Color code: 00FF00, Next search starts at: 35
// Found: #0000FF, Color code: 0000FF, Next search starts at: 53

4.3. `String.prototype.match()`: 일치 항목 배열로 가져오기

문자열에서 Regex와 일치하는 부분을 찾아 배열로 반환합니다. `g` 플래그의 유무에 따라 반환 값이 완전히 달라집니다.

  • g 플래그가 없을 때: 첫 번째 일치 항목에 대한 상세 정보(일치한 전체 문자열, 캡처링 그룹, 인덱스 등)가 담긴 배열을 반환합니다. `exec()`의 첫 번째 결과와 유사합니다.
  • g 플래그가 있을 때: 문자열 전체에서 일치하는 모든 부분을 찾아 '문자열만' 담긴 배열을 반환합니다. 캡처링 그룹이나 인덱스 정보는 포함되지 않습니다.
const log = "ERROR: User not found. INFO: Connected. WARNING: Deprecated API.";
// g 플래그가 있을 때: 모든 일치 항목을 배열로
const allLevels = log.match(/(ERROR|INFO|WARNING)/g);
console.log(allLevels); // ["ERROR", "INFO", "WARNING"]

// g 플래그가 없을 때: 첫 번째 일치 항목의 상세 정보
const firstLevelDetails = log.match(/(ERROR|INFO|WARNING): (.*)/);
console.log(firstLevelDetails[0]); // "ERROR: User not found." (전체 일치)
console.log(firstLevelDetails[1]); // "ERROR" (첫 번째 캡처링 그룹)
console.log(firstLevelDetails[2]); // "User not found." (두 번째 캡처링 그룹)

4.4. `String.prototype.matchAll()`: 모든 상세 정보를 반복자로

`g` 플래그가 있는 `match()`의 단점(캡처링 그룹 정보를 주지 않음)과 `exec()`를 루프로 돌려야 하는 번거로움을 한 번에 해결하는 현대적인 메서드입니다. `g` 플래그가 필수이며, 모든 일치 항목에 대한 상세 정보를 포함하는 이터레이터(iterator)를 반환합니다.

const text = "User: John (ID: 123), User: Jane (ID: 456)";
const userRegex = /User: (\w+) \(ID: (\d+)\)/g; // g 플래그 필수!

const matches = text.matchAll(userRegex);

for (const match of matches) {
  console.log(`Full match: ${match[0]}, Name: ${match[1]}, ID: ${match[2]}`);
}
// 출력:
// Full match: User: John (ID: 123), Name: John, ID: 123
// Full match: User: Jane (ID: 456), Name: Jane, ID: 456

// 배열로 변환하여 사용도 가능
const allMatchesArray = [...text.matchAll(userRegex)];
console.log(allMatchesArray[1][1]); // "Jane"

4.5. `String.prototype.replace()`: 찾아서 바꾸기의 제왕

문자열에서 Regex와 일치하는 부분을 다른 문자열로 대체합니다. 두 번째 인자로 문자열뿐만 아니라 함수를 전달할 수 있어 매우 강력합니다.

대체 문자열 사용

특별한 패턴($1, $2, $& 등)을 사용하여 캡처링 그룹이나 일치한 전체 문자열을 참조할 수 있습니다.

const date = "2023-10-26";
// YYYY-MM-DD -> DD/MM/YYYY 형식으로 변경
const reorderedDate = date.replace(/(\d{4})-(\d{2})-(\d{2})/, "$3/$2/$1");
console.log(reorderedDate); // "26/10/2023"

const text = "Markdown uses **bold** and *italic* text.";
let html = text.replace(/\*\*(.*?)\*\*/g, "$1");
html = html.replace(/\*(.*?)\*/g, "$1");
console.log(html); // "Markdown uses bold and italic text."

대체 함수 사용

대체할 내용을 동적으로 결정해야 할 때 함수를 전달합니다. 함수는 인자로 `(전체일치문자열, 캡처그룹1, 캡처그룹2, ..., 오프셋, 원본문자열)`을 받습니다.

// URL 쿼리스트링에서 kebab-case 파라미터를 camelCase로 변환하기
const url = "/api/users?sort-by=name&page-number=1";

function kebabToCamel(match, p1, p2) {
  return p1 + p2.toUpperCase();
}

const newUrl = url.replace(/-(\w)/g, kebabToCamel);
console.log(newUrl); // "/api/users?sortBy=name&pageNumber=1"

4.6. `String.prototype.split()`: 패턴으로 문자열 분할

단순 문자열뿐만 아니라 Regex를 기준으로 문자열을 분할하여 배열을 만들 수 있습니다.

const csvData = "apple, banana , orange; grape";
// 쉼표, 세미콜론, 그리고 주변의 공백을 모두 구분자로 취급
const fruits = csvData.split(/\s*[,;]\s*/);
console.log(fruits); // ["apple", "banana", "orange", "grape"]

5. 실전 Regex 레시피: 문제 해결을 위한 패턴 모음

이제 이론을 바탕으로 실제 프로젝트에서 마주칠 법한 문제들을 해결하는 Regex 레시피들을 소개합니다.

레시피 1: 비밀번호 강도 검사 (룩어라운드 활용)

회원가입 폼에서 '최소 8자, 대문자, 소문자, 숫자, 특수문자 각 1개 이상 포함' 같은 복잡한 규칙을 검증할 때 룩어라운드는 최고의 도구입니다.

function checkPasswordStrength(password) {
  // 패턴 해설:
  // ^                   : 문자열 시작
  // (?=.*[a-z])         : 소문자가 최소 1개 있는지 확인 (전방 탐색)
  // (?=.*[A-Z])         : 대문자가 최소 1개 있는지 확인 (전방 탐색)
  // (?=.*\d)            : 숫자가 최소 1개 있는지 확인 (전방 탐색)
  // (?=.*[@$!%*?&])    : 특수문자가 최소 1개 있는지 확인 (전방 탐색)
  // .{8,}               : 전체 길이가 최소 8자인지 확인
  // $                   : 문자열 끝
  const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/;
  return strongPasswordRegex.test(password);
}

console.log(checkPasswordStrength("weak"));             // false
console.log(checkPasswordStrength("GoodPassword123"));  // false (특수문자 없음)
console.log(checkPasswordStrength("Strong!Pass1"));     // true

레시피 2: URL 완전 분해 (이름 가진 캡처 그룹 활용)

URL을 프로토콜, 호스트, 경로, 쿼리 등으로 정교하게 분해하고 싶을 때, 이름 가진 캡처 그룹((?<name>...))을 사용하면 결과를 훨씬 직관적으로 다룰 수 있습니다.

function parseUrl(url) {
  const urlRegex = /^(?<protocol>https?):\/\/(?<hostname>[\w.-]+)(?::(?<port>\d+))?(?<pathname>\/[^?#]*)?(?<search>\?[^#]*)?(?<hash>#.*)?$/;
  const match = urlRegex.exec(url);
  
  if (!match) {
    return null;
  }
  
  return match.groups; // .groups 속성으로 이름 붙인 결과에 바로 접근 가능
}

const url = "https://www.example.co.kr:8080/path/to/resource?id=123&type=user#section-one";
const parsed = parseUrl(url);
console.log(parsed);
/*
{
  protocol: "https",
  hostname: "www.example.co.kr",
  port: "8080",
  pathname: "/path/to/resource",
  search: "?id=123&type=user",
  hash: "#section-one"
}
*/

레시피 3: 유튜브 비디오 ID 추출

다양한 형태의 유튜브 URL에서 비디오 ID만 정확히 추출하는 작업은 Regex의 좋은 활용 사례입니다.

function getYouTubeId(url) {
    // 다양한 유튜브 URL 패턴을 처리
    // 1. youtu.be/ID
    // 2. youtube.com/watch?v=ID
    // 3. youtube.com/embed/ID
    // 4. youtube.com/v/ID
    const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([\w-]{11})/;
    const match = url.match(youtubeRegex);
    return match ? match[1] : null;
}

console.log(getYouTubeId("https://www.youtube.com/watch?v=dQw4w9WgXcQ")); // dQw4w9WgXcQ
console.log(getYouTubeId("https://youtu.be/dQw4w9WgXcQ"));              // dQw4w9WgXcQ
console.log(getYouTubeId("https://www.youtube.com/embed/dQw4w9WgXcQ")); // dQw4w9WgXcQ

6. 전문가의 길: 성능, 함정, 그리고 철학

6.1. 재앙적 백트래킹(Catastrophic Backtracking)을 피하라

잘못 작성된 Regex는 시스템을 멈추게 할 수 있습니다. 이를 'Regex 서비스 거부 공격(ReDoS)'이라고도 합니다. 주로 중첩된 수량자와 불분명한 패턴이 만날 때 발생합니다. 엔진이 일치 항목을 찾기 위해 경우의 수를 기하급수적으로 탐색하다가 무한 루프에 가까운 상태에 빠지는 현상입니다.

위험한 패턴의 예: /^(a+)+$/

이 패턴에 "aaaaaaaaaaaaaaaaaaaaaaaa! "와 같이 끝이 일치하지 않는 긴 문자열을 입력하면, Regex 엔진은 a+를 묶는 다양한 모든 경우의 수를 테스트하느라 엄청난 시간을 소모하게 됩니다.

예방책:

  • 중첩된 수량자(e.g., (a+)*, (a*)*)를 피하세요.
  • 패턴을 최대한 구체적으로 작성하세요. .* 대신 [^<]* 처럼 명확한 부정을 사용하세요.
  • 게으른 수량자(*?, +?)나 비캡처링 그룹((?:...))을 적절히 활용하세요.

6.2. Regex는 만병통치약이 아니다

망치를 든 사람에게는 모든 것이 못으로 보인다는 말이 있습니다. Regex에 익숙해지면 모든 문자열 문제를 Regex로 해결하려는 유혹에 빠질 수 있습니다. 하지만 항상 최선의 선택은 아닙니다.

  • 단순 작업에는 단순 메서드를: 특정 문자열 포함 여부는 .includes(), 시작/끝 여부는 .startsWith()/.endsWith()가 훨씬 빠르고 가독성이 좋습니다.
  • HTML/XML/JSON 파싱에는 전용 파서를: Regex로 복잡한 HTML을 파싱하려는 시도는 재앙적 백트래킹과 엣지 케이스 처리 실패로 이어지기 쉽습니다. 브라우저 환경에서는 DOMParser를, Node.js 환경에서는 jsdom, cheerio 같은 검증된 라이브러리를 사용하세요. JSON은 JSON.parse()를 사용해야 합니다.

6.3. 가독성과 유지보수를 생각하라

복잡한 Regex는 작성한 사람조차 몇 달 뒤에는 해독하기 어렵습니다. 팀 프로젝트에서는 더욱 심각한 문제가 됩니다.

  • 주석을 활용하세요: 코드에 주석을 달아 각 패턴 부분이 어떤 역할을 하는지 설명하세요.
  • 패턴을 분리하고 조합하세요: 매우 복잡한 패턴은 여러 개의 작은 패턴으로 분리한 뒤, 문자열로 조합하여 new RegExp()로 생성하는 것도 좋은 방법입니다.
  • 온라인 도구를 적극 활용하세요: Regex101, RegExr 같은 사이트는 패턴을 시각적으로 분석해주고, 각 부분이 어떻게 동작하는지 상세히 설명해주므로 디버깅과 학습에 필수적입니다.

마치며: 암호를 넘어 언어로

이 긴 여정을 통해 우리는 정규표현식이 더 이상 해독 불가능한 암호가 아니라, 문자열의 세계를 탐험하고 조작하는 정교하고 논리적인 '언어'임을 확인했습니다. 처음에는 낯설고 복잡하게 느껴질 수 있지만, 이 글에서 다룬 개념과 문법, 실전 예제들을 꾸준히 연습하고 실제 프로젝트에 적용하다 보면 어느새 문자열 처리 작업에 대한 강력한 자신감을 갖게 될 것입니다.

이제 구글에서 의미도 모른 채 패턴을 복사해 붙여넣던 시절과는 작별을 고할 시간입니다. 문제를 정확히 분석하고, 필요한 패턴을 직접 설계하며, 성능과 가독성까지 고려하는 진정한 문자열 전문가로 거듭나세요. 정규표현식이라는 강력한 무기를 당신의 개발 무기고에 추가한 것을 축하합니다.


0 개의 댓글:

Post a Comment