Showing posts with label 정규표현식. Show all posts
Showing posts with label 정규표현식. Show all posts

Sunday, September 15, 2019

실무에서 바로 쓰는 정규표현식: 연속된 문자, 숫자 완벽하게 찾아내기

개발을 하다 보면 문자열 데이터 속에서 특정 패턴을 찾아야 하는 경우가 비일비재합니다. 사용자 아이디의 유효성을 검사하거나, 비밀번호 정책을 강제하거나, 혹은 잘못 입력된 데이터를 정리해야 할 때가 대표적입니다. 예를 들어, 'test0000'처럼 동일한 숫자가 과도하게 반복되는 아이디를 막거나, 'password111'과 같이 취약한 비밀번호를 걸러내고 싶을 수 있습니다. 또한, 텍스트 편집기에서 사용자가 실수로 '안녕하세요오오'라고 입력한 오타를 '안녕하세요'로 교정해주는 기능도 필요할 수 있습니다.

이러한 문제들을 해결하기 위해 매번 반복문(for, while)과 조건문(if)을 조합하여 코드를 작성하는 것은 매우 번거롭고 비효율적입니다. 코드가 길어지고, 가독성이 떨어지며, 다양한 예외 케이스를 처리하기 위해 로직은 점점 더 복잡해질 것입니다. 바로 이럴 때, '정규표현식(Regular Expression, 줄여서 Regex)'이 강력한 해결사로 등장합니다.

정규표현식은 문자열의 특정 패턴을 표현하는 언어입니다. 단 몇 줄의 코드로 복잡한 문자열 검색, 치환, 추출 작업을 놀랍도록 간결하고 우아하게 처리할 수 있습니다. 이번 글에서는 그중에서도 매우 실용적이면서도 강력한 기능인 '연속으로 반복되는 문자나 숫자'를 찾아내는 정규표현식 패턴에 대해 아주 깊이 있게 파헤쳐 보겠습니다. 이 글을 끝까지 읽으시면, 단순히 패턴을 복사해서 붙여넣는 수준을 넘어, 그 원리를 완벽하게 이해하고 여러분의 코드에 자유자재로 응용할 수 있는 능력을 갖추게 될 것입니다.


1. 문제 정의: 우리는 왜 '반복'을 찾아야 하는가?

본격적인 정규표현식 탐구에 앞서, 우리가 해결하려는 문제가 무엇인지 구체적인 시나리오를 통해 명확히 해봅시다. '연속된 문자 반복'이라는 패턴은 다양한 애플리케이션에서 중요한 검사 항목이 됩니다.

  • 사용자 입력 데이터 유효성 검사 (Input Validation)
    • 아이디/닉네임 생성 규칙: 'aaaaa', 'user0000' 등 성의 없거나 의미 없는 아이디 생성을 방지하여 서비스 품질을 유지합니다.
    • 게시글 및 댓글 작성: 'ㅋㅋㅋㅋㅋㅋㅋ', '!!!!!!!' 와 같이 특정 문자를 무의미하게 반복하여 도배하는 행위를 제한할 수 있습니다.
  • 비밀번호 보안 강화 (Password Security)
    • 가장 대표적인 사용 사례입니다. 'pass1111'이나 'qwertyzzz'처럼 동일한 문자나 숫자가 연속으로 3번 이상 나타나는 비밀번호는 추측하기 쉬워 매우 취약합니다. 대부분의 서비스에서는 이러한 패턴을 금지하는 정책을 가지고 있습니다.
  • 데이터 정제 및 클렌징 (Data Cleansing)
    • 사용자의 오타나 기계적인 오류로 인해 데이터가 잘못 입력되는 경우가 있습니다. 예를 들어, 'Hellooo World'를 'Hello World'로, '미팅 장소는 강남역 1번 출구구구'를 '강남역 1번 출구'로 교정하는 작업에 활용될 수 있습니다.
  • 로그 분석 및 시스템 모니터링 (Log Analysis)
    • 시스템 로그에서 비정상적으로 반복되는 특정 에러 코드나 메시지 시퀀스를 찾아내어 시스템의 이상 징후를 조기에 발견할 수 있습니다. 예를 들어, 'FAIL FAIL FAIL FAIL'과 같은 패턴을 감지할 수 있습니다.
  • 자연어 처리 및 텍스트 마이닝 (NLP & Text Mining)
    • 텍스트 데이터에서 강조나 감정 표현(예: '정말 대박이다ㅏㅏㅏ')을 분석하거나, 반대로 정규화(Normalization) 과정에서 이러한 반복을 제거하여 분석의 정확도를 높이는 데 사용됩니다.

이처럼 반복되는 문자를 찾는 기술은 단순히 재미있는 코딩 트릭이 아니라, 안정적이고 신뢰성 높은 소프트웨어를 구축하기 위한 핵심적인 요소 중 하나입니다. 이제, 이 모든 문제를 해결해 줄 마법 같은 정규표현식을 만나보겠습니다.


2. 핵심 원리: 마법의 패턴 /(\w)\1+/g 완전 해부

연속된 문자나 숫자를 찾는 가장 기본적인 정규표현식은 바로 /(\w)\1+/g 입니다. 처음 보면 암호처럼 보일 수 있지만, 각 구성 요소의 의미를 하나씩 뜯어보면 놀랍도록 논리적이고 간단합니다. 이 패턴을 완벽히 이해하는 것이 이번 여정의 핵심입니다.

/ ( \w ) \1 + / g

이 패턴을 6개의 조각으로 나누어 세밀하게 분석해 보겠습니다.

2.1. 슬래시 (/): 패턴의 시작과 끝

JavaScript에서 정규표현식 리터럴은 슬래시(/)로 시작하고 슬래시로 끝납니다. 즉, /.../는 "이 안에 있는 내용이 정규표현식 패턴이다"라고 인터프리터에게 알려주는 약속입니다.

2.2. 괄호 ( ... ): '기억'을 위한 캡처 그룹(Capturing Group)

정규표현식에서 괄호는 매우 특별하고 강력한 역할을 합니다. 괄호로 감싸인 부분은 '캡처 그룹'이 됩니다. 캡처 그룹의 핵심 기능은 '그룹 안의 패턴과 일치하는 문자열을 기억(캡처)하는 것'입니다.

예를 들어, (abc)라는 패턴은 'abc'라는 문자열과 일치하며, 일치하는 순간 'abc'를 메모리에 저장해 둡니다. 이 '기억' 기능은 나중에 다시 참조하기 위해 사용되며, 이것이 바로 우리가 살펴볼 '역참조'의 기반이 됩니다.

2.3. 역슬래시 w (\w): 단어 문자(Word Character)

\w는 '단어 문자'를 의미하는 정규표현식의 약칭(shorthand)입니다. 이것은 다음 문자의 집합과 동일한 의미를 가집니다.

  • 알파벳 대문자 (A-Z)
  • 알파벳 소문자 (a-z)
  • 숫자 (0-9)
  • 언더스코어 (_)

즉, \w[A-Za-z0-9_]와 정확히 같습니다. 우리의 패턴 (\w)는 "알파벳, 숫자, 언더스코어 중 하나의 문자와 일치하고, 그 문자를 첫 번째 캡처 그룹으로 기억하라"는 의미가 됩니다.

만약 문자열 'apple'을 이 패턴으로 검사한다면, 엔진은 다음과 같이 동작합니다.

  1. 'a'를 만납니다. \w와 일치합니다. 'a'를 첫 번째 캡처 그룹(\1)에 저장합니다.
  2. 'p'를 만납니다. \w와 일치합니다. 'p'를 첫 번째 캡처 그룹에 저장합니다. (이전 값 'a'는 덮어쓰여집니다.)
  3. ... 이런 식으로 계속 진행됩니다.

아직은 각각의 문자와 한 번씩만 매칭되고 있습니다. 이제 '반복'을 찾아내는 마법이 등장할 차례입니다.

2.4. 역슬래시 1 (\1): 마법의 열쇠, 역참조(Backreference)

\1은 '역참조(Backreference)'라고 불리며, 정규표현식의 가장 강력한 기능 중 하나입니다. 이것의 의미는 "첫 번째(1) 캡처 그룹이 기억하고 있는 바로 그 내용과 정확히 일치하는 문자"를 찾으라는 명령입니다.

앞서 (\w)가 문자를 찾아 기억한다고 했습니다. \1은 그 기억된 문자를 다시 불러와 사용하는 것입니다.

이제 (\w)\1 이라는 패턴을 'apple'과 'aa'에 적용해 봅시다.

  • 'apple'의 경우:
    1. 첫 번째 문자 'a'를 만납니다. (\w)가 'a'와 일치하고, 'a'를 \1에 기억합니다.
    2. 다음으로 \1을 검사할 차례입니다. \1은 'a'를 의미합니다. 하지만 문자열의 다음 문자는 'p'입니다. 'a'와 'p'는 다르므로 일치하지 않습니다. 패턴 매칭이 실패하고 처음부터 다시 시작합니다.
    3. 두 번째 문자 'p'부터 다시 시작합니다. (\w)가 'p'와 일치하고, 'p'를 \1에 기억합니다.
    4. 다음으로 \1('p'를 의미)을 검사합니다. 문자열의 다음 문자는 'p'입니다. 일치합니다! 하지만 그 다음이 없으므로 'pp'는 일치하지 않습니다. (만약 'apple'이 아니라 'appple'이었다면 'pp'가 일치했을 것입니다)
  • 'aa'의 경우:
    1. 첫 번째 문자 'a'를 만납니다. (\w)가 'a'와 일치하고, 'a'를 \1에 기억합니다.
    2. 다음으로 \1('a'를 의미)을 검사합니다. 문자열의 다음 문자는 'a'입니다. 'a'와 'a'는 일치합니다!
    3. 결과적으로, (\w)\1 패턴은 'aa'라는 문자열과 성공적으로 일치합니다.

이것이 바로 '연속된 동일한 문자'를 찾는 핵심 원리입니다. 첫 번째 문자를 기억하고, 바로 다음 문자가 기억된 문자와 같은지 비교하는 것입니다.

2.5. 플러스 (+): 하나 이상의 반복 (Quantifier)

+는 '수량자(Quantifier)'라고 불리며, 바로 앞에 있는 패턴이 1번 이상 반복되는 경우를 찾습니다. 즉, "as many times as possible, but at least once"의 의미입니다.

우리의 패턴에서 +\1 바로 뒤에 붙어있습니다. 따라서 \1+는 "첫 번째 캡처 그룹에서 기억한 문자가 1번 이상 연속으로 나타나는 부분"을 의미합니다.

이제 전체 패턴 (\w)\1+를 'helooo'라는 문자열에 적용해 보겠습니다.

  1. 'h', 'e', 'l'까지는 일치하는 반복이 없어 넘어갑니다.
  2. 첫 번째 'o'를 만납니다. (\w)가 'o'와 일치하고, 'o'를 \1에 기억합니다.
  3. 이제 \1+를 검사할 차례입니다.
    • 다음 문자는 'o'입니다. \1('o'를 의미)과 일치합니다. (1번 반복)
    • 그 다음 문자도 'o'입니다. \1('o'를 의미)과 다시 일치합니다. (2번 반복)
  4. \1이 총 2번 반복되었습니다. +(1번 이상 반복) 조건을 만족합니다.
  5. 따라서, (\w)에 일치하는 'o'와, \1+에 일치하는 'oo'가 합쳐져, 최종적으로 'ooo'라는 문자열이 이 패턴과 일치하게 됩니다.

2.6. g 플래그 (/g): 전역 검색 (Global Search)

마지막으로 패턴의 끝에 붙는 g는 '플래그(flag)'라고 불리며, 검색 옵션을 지정합니다. g'전역(Global)' 검색을 의미합니다.

만약 g 플래그가 없다면, 정규표현식 엔진은 패턴과 일치하는 첫 번째 결과만 찾고 검색을 종료합니다. 하지만 g 플래그가 있으면, 문자열 전체를 스캔하여 패턴과 일치하는 모든 결과를 찾아냅니다.

예를 들어, 'aaabbc_111'이라는 문자열에 /(\w)\1+/ (g 없음)를 적용하면 'aaa'만 찾아내고 멈춥니다. 하지만 /(\w)\1+/g (g 있음)를 적용하면 'aaa', 'bb', '111'을 모두 찾아냅니다.

이것으로 우리는 /(\w)\1+/g라는 패턴의 모든 구성 요소를 완벽하게 이해했습니다. 다시 한번 정리하면 다음과 같습니다.

/(\w)\1+/g : 문자열 전체(g)에서, 단어 문자(\w)가 나온 뒤, 바로 그 문자(\1)가 1번 이상 연속으로 반복(+)되는 모든 부분을 찾아라. 이때, 첫 번째 단어 문자는 기억(())해 두어야 한다.


3. JavaScript 실전 예제: 다양한 메서드 활용법

원리를 이해했으니 이제 JavaScript에서 이 정규표현식을 실제로 어떻게 활용하는지 다양한 메서드를 통해 알아보겠습니다. 각 메서드는 고유한 특징과 반환 값을 가지므로, 상황에 맞게 적절한 것을 선택하는 것이 중요합니다.


// 테스트에 사용할 정규표현식과 문자열
const pattern = /(\w)\1+/g;
const text = 'Heeelloo, thiiis is a teest string with reepeated characters... and numbers 111, 22, 3333.';

3.1. String.prototype.match(): 모든 일치 항목 배열로 얻기

match() 메서드는 정규표현식과 일치하는 부분을 검색합니다. g 플래그가 있을 때와 없을 때 동작이 다릅니다.

g 플래그 사용 시:

g 플래그가 있으면, 일치하는 모든 문자열을 담은 배열을 반환합니다. 일치하는 것이 없으면 null을 반환합니다. 이는 가장 일반적으로 사용되는 방법입니다.


const pattern = /(\w)\1+/g;
const text = 'Heeelloo, thiiis is a teest string with reepeated characters... and numbers 111, 22, 3333.';

const matches = text.match(pattern);

console.log(matches);
// 결과: [ "eee", "ll", "oo", "iii", "s", "s", "ee", "t", "ee", "111", "22", "3333" ]

보시는 바와 같이, 문자열 내에서 연속으로 반복되는 모든 부분을 정확하게 찾아내어 배열로 만들어 줍니다. 이를 이용해 "반복되는 문자가 3개 이상인 경우"를 필터링하는 등의 추가 작업을 쉽게 할 수 있습니다.


const longRepeats = matches.filter(match => match.length >= 3);
console.log(longRepeats);
// 결과: [ "eee", "iii", "111", "3333" ]

g 플래그 미사용 시:

g 플래그가 없으면, 첫 번째로 일치하는 부분에 대한 상세 정보를 담은 배열을 반환합니다. 이 배열에는 전체 일치 문자열 외에, 각 캡처 그룹의 내용, 인덱스 등의 추가 정보가 포함됩니다.


const patternWithoutG = /(\w)\1+/; // g 플래그 제거
const firstMatchInfo = text.match(patternWithoutG);

console.log(firstMatchInfo);
// 결과:
// [
//   "eee",      // 0: 전체 일치 문자열 (전체 패턴과 매칭된 부분)
//   "e",        // 1: 첫 번째 캡처 그룹 (\w)가 캡처한 내용
//   index: 1,      // 일치하는 부분의 시작 인덱스
//   input: "Heeelloo, thiiis is a teest...", // 원본 문자열
//   groups: undefined
// ]

이처럼 g 플래그 없이 사용하면, 단순히 일치 여부뿐만 아니라 어떤 문자가('e') 반복되었는지, 그리고 어디서(index: 1) 시작되었는지 등의 훨씬 상세한 정보를 얻을 수 있습니다.

3.2. RegExp.prototype.test(): 존재 여부만 빠르게 확인하기

test() 메서드는 문자열이 정규표현식과 일치하는지 여부만 확인하고 싶을 때 사용합니다. true 또는 false를 반환하므로, 조건문에서 간단하게 사용하기 좋습니다.


const pattern = /(\w)\1+/; // g 플래그는 test()와 함께 사용할 때 주의가 필요합니다.
const weakPassword1 = "password111";
const weakPassword2 = "mylovelysun";
const strongPassword = "abc_123_def";

console.log(`"${weakPassword1}"에 반복 문자가 있나요?`, pattern.test(weakPassword1)); // true
console.log(`"${weakPassword2}"에 반복 문자가 있나요?`, pattern.test(weakPassword2)); // true ('l' 반복)
console.log(`"${strongPassword}"에 반복 문자가 있나요?`, pattern.test(strongPassword)); // false

주의: g 플래그가 있는 정규표현식 객체에 test()를 여러 번 호출하면 예상과 다르게 동작할 수 있습니다. 정규표현식 객체는 마지막으로 일치한 위치(lastIndex)를 기억하기 때문에, 다음 검색은 그 위치부터 시작합니다. 따라서 일관된 결과를 원한다면 test()를 사용할 때는 g 플래그를 빼거나, 매번 새로운 정규표현식 객체를 생성하는 것이 안전합니다.

3.3. RegExp.prototype.exec(): 상세 정보와 함께 모든 항목 순회하기

exec() 메서드는 match()(g 없음)와 유사하게 상세 정보를 반환하지만, g 플래그와 함께 사용될 때 진가를 발휘합니다. exec()는 호출될 때마다 다음 일치 항목을 찾아 반환하며, 더 이상 일치하는 것이 없으면 null을 반환합니다. 이를 이용해 while 루프 안에서 모든 일치 항목을 순회하며 상세 정보를 얻을 수 있습니다.


const pattern = /(\w)\1+/g; // g 플래그 필수!
const text = 'aa-bb-cc-111';
let matchInfo;

while ((matchInfo = pattern.exec(text)) !== null) {
  console.log(
    `전체 일치: "${matchInfo[0]}", ` +
    `반복된 문자: "${matchInfo[1]}", ` +
    `시작 위치: ${matchInfo.index}`
  );
}

// 결과:
// 전체 일치: "aa", 반복된 문자: "a", 시작 위치: 0
// 전체 일치: "bb", 반복된 문자: "b", 시작 위치: 3
// 전체 일치: "cc", 반복된 문자: "c", 시작 위치: 6
// 전체 일치: "111", 반복된 문자: "1", 시작 위치: 9

exec()match()보다 더 많은 제어권을 제공하며, 각 일치 항목의 위치와 캡처 그룹 내용을 모두 알아야 할 때 매우 유용합니다.

3.4. String.prototype.replace(): 찾아낸 패턴을 다른 문자열로 바꾸기

replace()는 정규표현식의 활용도를 극대화하는 메서드입니다. 패턴에 일치하는 부분을 다른 문자열로 바꿀 수 있습니다. 이때, 교체될 문자열 안에서 특별한 패턴($&, $1 등)을 사용하여 원본의 일치 정보를 재활용할 수 있습니다.

  • $&: 일치한 전체 문자열
  • $1, $2, ...: 첫 번째, 두 번째, ... 캡처 그룹의 내용

예제 1: 반복된 문자 압축하기 ('helooo' -> 'helo')

이것은 캡처 그룹과 역참조의 개념을 완벽히 보여주는 예제입니다. 우리는 (\w)\1+ 패턴을 사용하여 'ooo'를 찾은 다음, 그것을 캡처된 문자 \1 즉, 'o' 하나로 교체할 것입니다.


const pattern = /(\w)\1+/g;
const text = 'helooo woorld, I am verrry happyyy!';

// $1은 첫 번째 캡처 그룹, 즉 반복되는 문자를 가리킨다.
const compressedText = text.replace(pattern, '$1'); 

console.log(compressedText);
// 결과: "helo world, I am very happy!"

이 코드는 'ooo'를 찾아 $1('o')로 바꾸고, 'oo'를 찾아 $1('o')로, 'rr'을 $1('r')로, 'yyy'를 $1('y')로 바꿔줍니다. 단 한 줄의 코드로 매우 효과적인 데이터 정제가 가능합니다.

예제 2: 반복된 부분 강조하기

이번에는 찾은 부분을 대문자로 바꾸고 괄호로 감싸서 강조해 보겠습니다.


const pattern = /(\w)\1+/g;
const text = 'A mississippi river boat.';

const highlightedText = text.replace(pattern, (match) => `[${match.toUpperCase()}]`);

console.log(highlightedText);
// 결과: "A mi[SS]i[SS]ippi river boat."

replace()의 두 번째 인자로 함수를 전달하면, 더 복잡하고 동적인 치환 로직을 구현할 수 있습니다. 함수의 첫 번째 매개변수(match)는 일치한 전체 문자열($&와 동일)을 받습니다.


4. 패턴 확장 및 고급 응용 기술

기본 패턴 /(\w)\1+/g의 원리를 마스터했다면, 이제 여러분의 필요에 맞게 패턴을 자유자재로 변형하고 확장할 수 있습니다. 정규표현식의 진정한 힘은 이러한 유연성에 있습니다.

4.1. 검색 대상 문자 변경하기

\w(알파벳, 숫자, 언더스코어) 대신 다른 문자 집합을 대상으로 반복을 찾고 싶을 수 있습니다.

모든 문자(줄바꿈 제외)에서 반복 찾기: (.)\1+

.(점) 메타문자는 줄바꿈 문자(\n)를 제외한 모든 문자와 일치합니다. 이를 이용하면 특수문자나 공백의 반복도 찾아낼 수 있습니다.


const pattern = /(.)\1+/g;
const text = 'Wow!!! That is sooooo cool...  Right??';

console.log(text.match(pattern));
// 결과: [ "!!!", "ooooo", "...", "  ", "??" ]

\w를 사용했다면 '!!!', '...', ' ', '??'는 찾지 못했을 것입니다. (.)로 바꾸는 것만으로 검색 범위가 훨씬 넓어졌습니다.

숫자만으로 반복 찾기: (\d)\1+ 또는 ([0-9])\1+

\d는 숫자(digit)를 의미하며, [0-9]와 같습니다. 전화번호나 계좌번호 등에서 연속된 숫자를 찾을 때 유용합니다.


const pattern = /(\d)\1+/g;
const text = 'My phone number is 010-1111-2223';

console.log(text.match(pattern));
// 결과: [ "1111", "222" ]

특정 문자들로만 반복 찾기: ([abc])\1+

대괄호 [] 안에 원하는 문자들을 넣어 '문자 집합(character set)'을 만들 수 있습니다. 아래 예제는 a, b, c 중에서 반복되는 경우만 찾습니다.


const pattern = /([abc])\1+/g;
const text = 'aaabbbcccdddeee';

console.log(text.match(pattern));
// 결과: [ "aaa", "bbb", "ccc" ] 
// 'ddd'와 'eee'는 [abc] 집합에 포함되지 않으므로 무시됩니다.

4.2. 반복 횟수 제어하기: 수량자 {n,m}

+는 '1번 이상'을 의미했지만, 때로는 '정확히 3번' 또는 '2번에서 4번 사이'와 같이 더 정교하게 반복 횟수를 제어해야 합니다. 이때 중괄호 수량자 {}를 사용합니다.

  • {n}: 정확히 n번 반복
  • {n,}: 최소 n번 이상 반복
  • {n,m}: 최소 n번, 최대 m번 반복

3번 이상 연속되는 문자 찾기 (비밀번호 규칙)

이는 비밀번호 유효성 검사에서 매우 흔하게 사용되는 규칙입니다. 'aaa'나 '1111'은 허용하지 않는 경우입니다.


const pattern = /(\w)\1{2,}/; // \1이 2번 이상 반복, 즉 전체 문자는 3번 이상 연속
const password_ok = "pa55word";
const password_fail1 = "passwooorrd";
const password_fail2 = "1234444abc";

console.log(pattern.test(password_ok));    // false
console.log(pattern.test(password_fail1)); // true ('ooo' 때문에)
console.log(pattern.test(password_fail2)); // true ('4444' 때문에)

(\w)\1{2,}를 분석해보면, (\w)가 한 문자를 차지하고, \1{2,}가 그 문자의 2번 이상 반복을 의미하므로, 총 1 + 2 = 3번 이상 연속되는 문자를 찾게 됩니다.

정확히 2번 연속되는 문자만 찾기


const pattern = /(\w)\1{1}/g; // \1이 정확히 1번 반복. 즉, 전체 문자는 2번 연속
// 참고: {1}은 생략할 수 없으므로, (\w)\1 과는 다르게 동작할 수 있습니다. 
// 더 명확하게는 부정형 탐색을 사용해야 하지만, 기본적인 개념은 이렇습니다.
// 정확히 2번만 찾기 위한 더 정교한 패턴: /(\w)\1(?!\1)/g
// (?!\1)은 'lookahead' 문법으로, 뒤에 \1이 오지 않는 경우에만 일치하라는 뜻입니다.

const text = 'aa bbb cccc d ee';
const precisePattern = /(\w)\1(?!\1)/g;

console.log(text.match(precisePattern));
// 결과: [ "aa", "ee" ] 
// 'bbb'나 'cccc'는 뒤에 같은 문자가 또 오기 때문에 (?!\1) 조건에 걸려 제외됩니다.

4.3. 고급 활용 사례: 반복되는 '단어' 찾기

지금까지는 '문자'의 반복을 찾았습니다. 역참조를 응용하면 '단어'의 반복도 쉽게 찾을 수 있습니다. 예를 들어, "I love love this." 와 같은 문장에서 중복된 'love'를 찾는 것입니다.

패턴은 다음과 같습니다: /\b(\w+)\s+\1\b/g

  • \b: 단어 경계(Word Boundary). 단어의 시작이나 끝을 의미합니다. 공백, 구두점, 문자열의 시작/끝과 단어 문자 사이의 위치에 해당합니다. 이것이 없으면 'the theater'에서 'the'와 'theater'의 'the'를 반복으로 오인할 수 있습니다.
  • (\w+): 1개 이상의 단어 문자로 이루어진 '단어'를 캡처합니다.
  • \s+: 1개 이상의 공백 문자(스페이스, 탭, 줄바꿈 등). 단어와 단어 사이의 간격을 의미합니다.
  • \1: 첫 번째 캡처 그룹, 즉 앞에서 찾은 바로 그 '단어'를 의미합니다.
  • \b: 다시 단어 경계로 끝나야 완전한 단어 반복입니다.

const pattern = /\b(\w+)\s+\1\b/gi; // i 플래그로 대소문자 무시
const text = "This is a test test string. Paris in the the spring. Hello hello world!";

console.log(text.match(pattern));
// 결과: [ "test test", "the the", "Hello hello" ]

// 중복 단어 제거하기
const correctedText = text.replace(pattern, '$1'); // $1은 캡처된 단어
console.log(correctedText);
// 결과: "This is a test string. Paris in the spring. Hello world!"

이처럼 기본 원리를 응용하면 훨씬 더 복잡하고 유용한 패턴을 만들어 낼 수 있습니다.


5. 결론: 하나의 패턴, 무한한 가능성

우리는 오늘 /(\w)\1+/g라는 하나의 정규표현식으로 시작하여 그 내부의 동작 원리를 원자 단위까지 깊이 파고들었습니다. 캡처 그룹 ()의 '기억' 능력과 역참조 \1의 '재활용' 능력이 어떻게 결합하여 '반복'이라는 패턴을 찾아내는지 명확히 이해했습니다.

더 나아가, 우리는 이 기본 패턴을 다양한 상황에 맞게 변형하는 방법을 배웠습니다.

  • \w., \d, [] 등으로 바꾸어 검색 대상을 변경했습니다.
  • +{n,m} 수량자로 바꾸어 반복 횟수를 정교하게 제어했습니다.
  • JavaScript의 match, test, exec, replace 메서드를 활용하여 단순히 찾는 것을 넘어, 확인하고, 순회하고, 수정하는 실용적인 코드를 작성했습니다.
  • 마지막으로, 개념을 확장하여 반복되는 '문자'가 아닌 반복되는 '단어'를 찾는 고급 기술까지 살펴보았습니다.

정규표현식은 처음에는 외계어처럼 보일 수 있지만, 그 핵심 원리를 이해하고 나면 코드를 작성하는 방식을 근본적으로 바꾸는 강력한 도구가 됩니다. 복잡한 문자열 처리 로직을 단 한 줄의 표현식으로 압축할 때의 짜릿함은 개발자만이 느낄 수 있는 큰 즐거움 중 하나입니다.

오늘 배운 역참조(Backreference)의 개념을 잊지 마십시오. 이것은 정규표현식의 수많은 고급 기능으로 통하는 문을 열어주는 열쇠입니다. 이제 여러분의 코드에 흩어져 있는 비효율적인 문자열 처리 로직들을 이 우아하고 강력한 정규표현식으로 리팩토링해 보시는 것은 어떨까요? 하나의 패턴을 마스터한 여러분의 가능성은 이제 무한합니다.