[패턴 매칭] 정규표현식

Ninto·2023년 2월 20일
2

학습 기록

목록 보기
6/17

📌 정규 표현식

정규표현식(Regular Expression)은 줄여서 RegExp라고 합니다.

정규표현식 또는 정규식은 문자열에서 특정 문자 조합을 찾기 위한 패턴입니다. -MDN

const regExp = /abc/g;
typeof regExp; // 'object'

쉽게 말하자면, 정규표현식은 텍스트 패턴을 정의하는 객체입니다.

JavaScript의 RegExp 클래스는 정규표현식이며,
문자열과 RegExp 모두 정규표현식을 사용해 텍스트에서 패턴을 찾고 대체하는 메서드를 정의합니다.

  • RegExp의 exec()test()메서드
  • String의 match(), matchAll(), replace(), replaceAll(), search(), split()등의 메서드

정규표현식의 가장 큰 기능은 특정 패턴을 가지고, 문자열을 찾을 수 있다는 점 입니다.

대체적으로 문자열에서 특정 내용을 찾거나 대체 또는 발췌 하는데에 주로 사용합니다.

정규 표현식은 그 자체로 하나의 작은 프로그래밍 언어입니다.

따라서 RegExp API를 효율적으로 사용하려면 정규 표현식 문법을 사용해 텍스트 패턴을 정의하고 만드는 방법을 알아야 합니다.

정규표현식을 굳이 왜 사용해야 하나요? 🤔

만약, 비밀번호의 조건을 설정해야 하는 로직을 구현해야 한다고 했을 때
비밀번호의 조건으로 아래의 2가지가 주어졌다고 가정해봅시다.

  • 비밀번호는 8글자 이상, 16글자 이하여야 합니다.
  • 숫자, 문자, 특수문자가 모두 포함되어 있어야 합니다.
const isValidLength = (str) => {
  str.length >= 8 && str.length <= 16
}

const isIncludeNum = (str) => {
  str.some((character) => character >= '0' && character <= '9';)
}

const ...

간단한 조건이라면 이런식으로 함수 또는 if문을 사용해서 구현할 수 있습니다.

하지만, 조건이 점점 늘어날수록 코드는 길어지고 복잡해지는 문제가 발생하게 됩니다.

바로 이런 상황에서 정규표현식을 사용할 수 있습니다.

정규표현식의 장점은 패턴으로 검증이 가능하여 if문을 많이 쓰지 않아도 된다는 점입니다.

간단한 검증일때는 if문으로 해결하여 메서드 명을 통해 가독성을 높이고,
복잡한 검증이 있을 때만 정규표현식을 사용하는 것을 권장합니다.

정규표현식이 구체적으로 자주 사용되는 예시는 아래와 같습니다.

  • 컴파일러의 파서
  • CLI 환경을 주로 사용하는 경우, grep, sed, awk를 통해 쓰임
  • 이메일, 주소, 전화번호 규칙 검증
  • 입력에서 불필요한 입력 검증
  • 개발도구에서 문자열 치환
  • 로깅에서 찾아볼 때
  • 코딩 테스트 등...


📌 정규표현식 정의와 표현

자바스크립트에서는 RegExp 객체로 정규표현식을 표현합니다.

/패턴/플래그;

기본적으로 정규표현식은 패턴구분자 시작 + 작성할 패턴 + 패턴구분자 끝 + 패턴 변경자로 구성됩니다

정규표현식은 크게 메타문자와 수량자, 그룹화 등의 여러가지 기능을 가지고 있고, 옵션으로 뒤에 flag를 붙일 수 있습니다.

🔗 정규표현식 리터럴

// 문자열 리터럴
const str = "abc";

// 생성자로 정규표현식 생성
const regExp1 = new RegExp("abc");

// 자주 사용하는 문법
const regExp2 = /abc/;

문자열 리터럴이 따옴표 안에 문자를 쓰는 것과 마찬가지로,
정규 표현식 리터럴은 슬래시 (/) 한 쌍 안에 문자를 씁니다.

위 코드처럼 생성자를 통해 RegExp 객체를 생성할 수 있지만, 보통은 슬래쉬(/)로 감싸는 특별한 리터럴 문법을 더 자주 사용합니다.


🔗 정규표현식 메타문자

메타문자는 문자를 나타내는 문자를 의미합니다.

자주 사용하는 메타문자의미
.모든 문자 (숫자,문자,공백,특수문자 등...)
[]대괄호 안에 들어가 있는 문자를 찾습니다.
[^]대괄호 안에서 ^은 not을 의미합니다.
|or
\s공백 (tab과 space 등)
\d소문자 d는 숫자를 의미합니다. [0-9]
\D대문자 D는 숫자를 제외한 문자를 의미합니다.
\w소문자 w는 word의 약어로, 문자를 나타낼 때 사용합니다.(대문자, 소문자, 언더스코어까지 포함 [A-Za-z_])
\W대문자 W는 문자가 아닌 것 들을 의미합니다. (\w와 반대)
\bboundary의 약자로 위치(경계)를 나타냅니다.
^찾으려는 문자열의 시작 위치를 나타냅니다.
$찾으려는 문자열의 끝 위치를 나타냅니다.

이 중에서 \s\b를 살펴보자면,

둘 다 공백을 의미하지만
\s는 Selecting 되는 반면, \b는 Selecting에 포함되지 않습니다.


🔗 정규표현식 수량자

수량자는 앞 문자의 수를 제한할 수 있습니다.

/str{N, M}/ //str이 N번이상 M번 이하 반복됨을 뜻합니다.

기본적으로 문자 뒤에 중괄호{}를 사용하게 되면 문자의 수를 표현할 수 있습니다.

중괄호 앞의 문자열이 첫번째 숫자 이상, 두번째 숫자 이하 반복됨을 의미합니다.

수량자의미
a{n,m}a가 n번이상, m번 이하 반복됨을 뜻합니다.
{n,}앞 문자가 n개 이상임을 뜻합니다. (m이 생략된 형태)
{n}앞 문자가 n개
?앞에 있는 문자열이 없거나, 1개 임을 뜻합니다. {0,1}
+해당 문자열이 1번 이상 반복됨을 뜻합니다. {1,}
*앞에 문자가 0번 이상 반복됨을 뜻합니다. {0,}

🔗 정규표현식 Group

정규표현식에서는 Group이라는 기능을 소괄호()를 통해서 사용할 수 있습니다.

const regExr = /(\d{2})/g;
그룹의미
()소괄호로 감싼 문자열을 그룹핑하여 캡처링해줍니다.
(?:)캡처링을 제외하고 그룹핑만을 해줍니다.
(?<groupName>)<>안에 그룹의 이름을 지정할 수 있습니다. (match나 exec의 groups 프로퍼티에서 해당 name으로 접근이 가능)

기본적으로 소괄호로 문자열을 감싸면 해당 그룹을 캡처링 할 수 있습니다.
캡처링 된 그룹들은 matchexec메서드를 통해 확인할 수 있습니다.

그룹핑과 많이 혼동하는게 (?=)키워드 입니다.
해당 키워드는 전방 탐색 키워드로 일치영역을 발견해도 그 값을 반환하지 않습니다.


🔗 정규표현식 flags

자주 사용하는 플래그의미
iignore의 약자, 대소문자를 구분하지 않습니다.
gglobal의 약자, 전 구역을 복수로 검사합니다.
mmultiline의 약자, 다중행 검사를 합니다.

정규표현식은 기본적으로 슬래쉬 안에 패턴을 삽입하고,
flags를 옵션으로 삽입할 수 있습니다.


🔗 정규표현식 활용 메서드

  • String.prototype.match() : 정규식과 겹치는 전체 문자열을 첫번째 요소로 포함하는 Array를 반환한 다음, 괄호 안에 캡처된 결과를 리턴합니다.
"I am Super Man 12345".match(/\d/); // ['1', index: 15, input: 'I am Super Man 12345', groups: undefined]

"I am Super Man 12345".match(/\d/g); // ['1', '2', '3', '4', '5']

match와 group 네이밍을 활용해서 다음과 같이 주민번호에서 생년월일을 손 쉽게 추출해 낼 수 있습니다.

const { year, month, date } = "970201-1111111".match(
  /^(?<year>\d{2})(?<month>\d{2})(?<date>\d{2})/
).groups;
console.log(year, "년", month, "월", date, "일생"); // 97 년 02 월 01 일생
  • RegExp.prototype.test() : 주어진 문자열이 정규표현식을 만족하는지를 Boolean으로 반환해줍니다.
/^01[\d][-\s/]\d{3,4}[-\s/]\d{4}$/.test("010-1111-2222"); // true
/^01[\d][-\s/]\d{3,4}[-\s/]\d{4}$/.test("010-1111-222"); // false

const tel1 = "010-1234-567팔";
const tel2 = "010-1234-5678";

// 정규 표현식 리터럴로 휴대폰 전화번호 패턴을 정의한다.
const regExp = /^\d{3}-\d{4}-\d{4}$/;

// tel이 휴대폰 전화번호 패턴에 매칭하는지 테스트(확인)한다.
regExp.test(tel1); // false
regExp.test(tel2); // true

휴대폰 번호의 조건을 활용하여 해당 문자열이 조건을 만족하는지 확인할 수 있습니다.

const str = "ABC";
const regExp = /abc/i;
const regExp2 = /abc/;

regExp.test(str); // true;
regExp2.test(str); // false;
  • RegExp.prototype.exec() : 주어진 문자열에서 일치 탐색을 수행한 결과를 배열 혹은 null로 반환합니다.
const str = "IS this all there is?";
const regexp = /is/gi;
const regexp2 = /is/i;
const regexp3 = /is/;

str.match(regexp); //  ['IS', 'is', 'is']
str.match(regexp2); // ['IS', index: 0, input: 'IS this all there is?', groups: undefined]

regexp3.exec(str); // ['is', index: 5, input: 'IS this all there is?', groups: undefined]
regexp2.exec(str); // ['IS', index: 0, input: 'IS this all there is?', groups: undefined]
regexp.exec(str);
  • String.prototype.replace() : 패턴에 일치하는 문자열의 일부 또는 모든 부분을 교체한 문자열을 반환합니다.
const reg = /(-\d)(\d*)/
'980828-2345'.replace(reg, '*'); // '980828*' 뒷 번호를 *으로 변환

String.replace(reg, function(match, p1, p2, ..., offset, string) {
  return ...;
})

'980828-2345'.replace(reg, (match, p1, p2,) => p1 + ('*'.repeat(p2.length))); // '980828-2***'

replace의 두 번째 인자로 함수를 넣을 수 있습니다.

replace의 첫번째 인자로는 정규식을 담은 후 두번째 인자로 함수를 넣으면, 함수의 파라미터로 4가지 종류의 인자를 받을 수 있습니다.

  • match : 매치된 문자열
  • p1, p2, ... : 그룹화된 문자열 순서대로 인자로 받을 수 있습니다.
  • offset: 매치된 문자열의 index
  • string: 조사된 전체 문자열

위의 예시와 같이 찾은 문자열에 따라 동적으로 변환을 원할때 유용하게 사용할 수 있습니다.



📌 자바스크립트 RegExp 활용

정규표현식으로 작성을 완료한 테스트가 항상 옳을 수는 없습니다.
그렇기때문에, 테스트는 필수적이며 https://regexr.com 사이트를 이용하면 여러가지 테스트 케이스를 작성하면서 오류가 있는지 확인할 수 있습니다.

🔗 간단한 문자 조건설정

  • 영어 대문자, 소문자, 한글, 숫자가 한번 이상 반복되는 문자열을 전역 검색
const regExp = /[A-Za-z가-힣ㄱ-ㅎ0-9]+/g;
  • 문자열에서 알파벳 대문자, 숫자, 더하기(+), 밑줄(_), 마침표(.)를 제외한 모든 문자를 제거하시오.
function removeChar(input) {
  return input.replace(/[^A-Z0-9+_.]/g, "");
}

const input = "aAb0c+d_e.f";
console.log(removeChar(input)); // A0+_.
  • 문자열에서 마침표(.)가 3번 이상 연속된 부분을 하나의 마침표(.)로 치환하시오.
function changeDot(input2) {
  return input2.replace(/[.]{3,}/g, ".");
}

const input2 = "a...b...c..d.e";
console.log(changeDot(input2)); // a.b.c..d.e
  • 반복되는 문자열 검사
const target = "A AA B BB Aa Bb AAA";

// 'A'가 최소 1번, 최대 2번 반복되는 문자열을 전역 검색
const regExp = /A{1,2}/g;

target.match(regExp); //  ['A', 'AA', 'A', 'AA', 'A']

// 공백이 들어가있는 단어에 대해서만 찾음
const regExp2 = /A{1,2}\s/g;
target.match(regExp2); // ['A ', 'AA ']

// 'A'가 2개 이상 들어가 있는 문자열 검색
const regExp3 = /A{2,}/g;
target.match(regExp3); // ['AA', 'AAA']

🔗 휴대폰 번호 조건설정

  • 첫 세글자는 01x(x는 숫자)의 형태입니다.
  • 구분자는 -, 공백, /를 허용합니다.
  • 두번째 구역은 3글자 이상 4글자 이하 숫자입니다.
  • 마지막 구역은 4글자의 숫자로 이루어집니다.
const regExp = /^01\d[-\s/]\d{3,4}[-\s/]\d{4}$/g;

🔗 URL 조건설정

// https로 시작하는 문자열인지 테스트
const target = "https://www.naver.com";

const regExp = /^https/;
regExp.test(target); // true

// com으로 끝나는지 테스트
const regExp2 = /com$/;
regExp2.test(target); // true

// https:// 또는 http:// 로 시작하는지 테스트
// 안쪽에서 슬래쉬를 사용하고 싶을땐 이스케이프 사용
const regExp3 = /^https?:\/\//.test(target); // true
regExp3.test("httppps://"); // false

🔗 이메일 조건설정

const regExp = /^(?=[^_])+[₩w-.]+@[a-zA-Z0-9]+₩.[a-zA-Z0-9]+$/;

🔗 주민등록번호 조건설정

const regExp = /^[\d]{2}(?:(?:0[13578]|1[02])(?:[0-2]\d|3[01])|(?:02(?:[01]\d|2[0-9]))|(?:0[469]|11)(?:[0-2]\d|30))[-/.][1-4]\d{6}$/;

정규표현식은 복잡한 조건에서 사용할 수 있지만, 그렇다고 항상 좋은 점만 있는 것은 아닙니다.

위의 예시와 같이 가독성이 상당히 나쁜편입니다.
일반적으로 사용되는 문자열이 아닌 meta문자열을 사용하기 때문에 기본적으로 가독성이 떨어질 수 밖에 없습니다.

또한, 정규표현식 엔진은 대부분 백트래킹을 사용하기 때문에 최악의 경우 시간복잡도 O(2^n)이 나오게 됩니다.
따라서 일반 내장 메서드나 for문보다 성능상 떨어질 수 있습니다.

따라서 간단한 로직은 내장 메서드를 활용해서 함수 분리를 하는 것이 성능이나 가독성 면에서 더 유리하고, 복잡하고 복합적인 로직을 검사하고 싶을때는 정규표현식이 유용하게 사용됩니다.



📌 참고자료

https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Regular_Expressions

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/RegExp

https://www.youtube.com/watch?v=_eEZqTx5N7s&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=53

https://www.youtube.com/watch?v=CjoDIgDOHA4&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=193

https://blogpack.tistory.com/1131

https://velog.io/@semnil5202/%EC%A0%95%EA%B7%9C%ED%91%9C%ED%98%84%EC%8B%9D

https://kim6394.tistory.com/100

profile
성장에는 성장통이 있기 마련이다.

0개의 댓글