# 내가 실제로 사용하는 20개의 정규 표현식 패턴 (다른 200개는 대량 삭제한 후)
💡 주요 요점
- 정규 표현식 최대주의자에서 최소주의자로의 여정
- 정규 표현식이 거의 우리의 API를 무너뜨린 순간
- 실제로 중요한 것 분해하기
- 정리에서 살아남은 20개의 패턴
한때 나는 이메일 유효성 검사를 위한 847자 정규 표현식을 썼다. 그걸 위해 내 인생의 세 시간을 보냈고, 그 안에는 중첩된 뷰 미리보기와 문자 클래스 예외, 그리고 내 눈을 물넘치게 할 만큼의 백슬래시가 포함되었다. 나는 그것이 너무 자랑스러웠다. 팀 슬랙에 “이것이 모든 엣지 케이스를 처리합니다.”라는 오만한 메시지와 함께 올렸다.
그러자 누군가 내가 RFC 5322를 링크해 주었다.
행복하게도 모르는 사람들에게, RFC 5322는 공식 이메일 주소 사양이다. 모든 기술적으로 합법적인 이메일 주소를 검증하는 실제 완전한 정규 표현식 패턴은 6,000자 이상이다. 여기에는 괄호 안의 주석, 이스케이프된 문자가 포함된 인용 문자열, 대괄호 안의 도메인 리터럴과 같은 것들이 있다. 기술적으로, `"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com`은 그 규격에 따라 유효한 이메일 주소이다.
나는 내 847자 패턴을 응시했다. 그 다음 RFC를 봤고, 다시 내 패턴을 봤다. 그런 후에 나는 합리적인 개발자가 할 수 있는 일을 했다: `/.+@.+\..+/`로 바꾸고 내 삶을 이어갔다. 왜냐하면 — 실제로 그런 엣지 케이스를 사용하는 사람은 없기 때문이다. 그리고 만약 그것을 사용한다면, 그들은 어떤 문제가 생기든 감수해야 할 것이다.
그 일이 있은 지 5년이 지났다. 그 이후로, 나는 수백 개의 정규 표현식 패턴을 작성했다. 나는 선임 개발자를 울게 만든 정규 표현식을 디버깅했다. 운영에서 느린 속도를 일으킨 패턴을 최적화했다. 그리고 그 모든 과정을 통해, 나는 중요한 것을 배웠다: 대부분의 정규 표현식 패턴은 결코 필요하지 않은 쓰레기이다.
정규 표현식 최대주의자에서 최소주의자로의 여정
나는 일부 사람들이 우표를 수집하듯 정규 표현식 패턴을 수집했다. 나는 상상할 수 있는 모든 것에 대한 패턴이 있는 거대한 `regex-library.js` 파일을 가지고 있었다. 존 영역 ID가 있는 IPv6 주소. 룬 알고리즘 검증이 있는 신용카드 번호. 모든 희귀 프로토콜을 처리하는 URL. 1930년대 지역 번호 검증이 있는 사회 보장 번호.
그 파일은 3,200줄이었다. 나는 모든 프로젝트에서 시간을 절약할 유용한 것을 만들고 있다고 확신했다. 심지어 나는 예제와 성능 벤치마크가 포함된 문서 작성을 시작하기도 했다.
그리고 나는 직장을 바꿨다.
내 새 회사에서, 나는 내 사랑하는 정규 표현식 라이브러리를 코드베이스에 가져오려고 했다. 선임 건축가는 코드 리뷰 중에 그것을 보고 간단한 질문을 했다: "이 중에서 지난 6개월 동안 실제로 사용한 것은 어떤 것인가?"
나는 형광펜을 들고 파일을 살펴보았다. 200개 이상의 패턴 중에서, 나는 아마 15개를 사용했다. 나머지는 "혹시 필요할지도"라는 패턴 - 문제를 찾기 위해 해결책을 찾는 패턴이었다. 나는 그것들이 실제 문제를 해결하기 위한 것이 아닌, 지적으로 흥미로워 보이기 때문에 썼다.
그때 나는 내 대규모 정규 표현식 제거를 시작했다. 나는 모든 패턴을 살펴보고 물었다: "이것이 운영에서 필요했나? '혹시 언젠가 필요할지도'가 아니라, 실제로 필요했는가?" 대답이 아니면 삭제했다. 자비가 없다. "하지만 만약에" 예외도 없다.
파일은 3,200줄에서 400줄로 줄어들었다. 그리고 200줄로, 그리고 내가 실제로 정기적으로 사용하는 20개 패턴이 포함된 약 100줄로 줄어들었다. 그리고 너는 알지? 나는 다른 180개 패턴이 그리워한 적이 한 번도 없다. 전혀 그렇지 않다.
정규 표현식이 거의 우리의 API를 무너뜨린 순간
내가 정규 표현식으로 일으킨 최악의 운영 사건에 대해 이야기해보겠다. 우리는 사용자가 생성한 콘텐츠를 수용하는 API 엔드포인트가 있었다 - 기본적으로 사용자가 원하는 것을 쓸 수 있는 메모장 필드였다. 쉽고 간단하지 않은가?
하지만 우리는 텍스트에서 URL을 감지하고 자동으로 링크를 걸고 싶었다. 그래서 나는 URL을 일치시키되 잘못된 긍정지를 피할 수 있다고 생각했던 정교한 정규 표현식 패턴을 작성했다. 유효한 프로토콜을 검사하기 위한 뷰 미리보기, 도메인 이름을 위한 문자 클래스, 선택적 포트 번호, 경로 세그먼트, 쿼리 매개변수, 조각 식별자를 포함했다. 그것은 아름다웠고 포괄적이었다. 그리고 그것은 재앙적인 실수였다.
패턴은 테스트에서 잘 작동했다. 나는 다양한 URL을 던졌고, 그것은 완벽하게 처리했다. 우리는 금요일 오후에 운영에 배포했을 때 내가 꽤 뿌듯한 기분이었다. (그래, 나는 안다. 금요일에 배포하지 마라. 나는 그 교훈을 뼈저리게 체험했다.)
1시간도 안 되어, 우리의 API 응답 시간은 50ms에서 30초로 늘어났다. 그리고 타임아웃이 발생하기 시작했다. 우리의 모니터링은 크리스마스 트리처럼 빛났다. 사용자들이 불만을 표시했고, 내 전화가 울리기 시작했다. 상황이 나빴다.
범인은? 한 사용자가 재앙적인 백트래킹을 촉발하는 패턴이 포함된 긴 문자열을 붙여넣었다. 정규 표현식 엔진은 가능한 모든 일치 조합을 시도하고 있었고, 5,000자 입력 문자열로는 수십억 번의 시도를 의미했다. 각 요청은 타임아웃이 발생하기 전 30초 이상 CPU 코어를 100%로 고정시켰다.
우리는 즉시 롤백했고, 나는 그 패턴을 다시 작성하는 데 주말을 보냈다. 새로운 버전은 더 간단하고 덜 "정교"하며 반복에 대한 명시적인 제한이 있었다. 그것은 모든 가능한 URL 형식을 집어내지 않았다 - 사람들이 실제로 사용하는 99.9%의 URL을 잡았다. 그리고 그것은 초단위가 아닌 마이크로초로 실행되었다.
그 사건은 나에게 중요한 것을 가르쳐주었다: 정규 표현식 복잡성은 자산이 아닌 부담이다. 패턴이 화려할수록 운영에서 문제가 발생할 가능성이 더 높아진다. 일반적인 경우를 처리하는 단순한 패턴이 모든 엣지 케이스를 처리하는 복잡한 패턴보다 거의 항상 낫다.
실제로 중요한 것 분해하기
수년 동안 정규 표현식을 작성하고 내 실수로부터 배운 후, 나는 유지할 가치가 있는 패턴을 결정하기 위한 간단한 프레임워크를 개발했다. 이는 세 가지 기준으로 간결해진다:
빈도: 이 패턴을 월 1회 이상 사용하나? 그렇지 않다면, 필요할 때 구글 검색하면 된다. 드물게 사용되는 패턴을 외우거나 유지하는 것은 아무런 의미가 없다. 신뢰성: 이 패턴이 서로 다른 정규 표현식 엔진 전반에서 일관되게 작동하는가? JavaScript, Python, Go는 모두 약간씩 다른 정규 표현식 구현을 가지고 있다. 화려한 기능에 의존하는 패턴은 이식성이 없을 수 있다. 성능: 이 패턴이 선형 시간에 실행되나, 아니면 재앙적인 백트래킹을 유발할 수 있는가? 나는 중첩된 수량자와 중복 대안을 두렵게 여기는 것을 배웠다.이 기준을 사용하여, 대부분의 패턴은 통과하지 못한다. 시간대 오프셋 및 주 번호를 가진 ISO 8601 날짜를 구문 분석하는 화려한 정규 표현식은? 빈도 검사에서 실패한다 - 나는 아마 연간 두 번 필요하며, 필요할 때 찾아볼 수 있다. IBAN 은행 계좌 번호를 검증하는 패턴은? 신뢰성 검사에서 실패한다 - 너무 복잡해서 나 스스로 그것을 유지할 수 있다고 믿지 않는다. 중첩 괄호와 일치하는 재귀 패턴은? 성능 검사에서 실패한다 - 그것은 발생할 재앙적인 백트래킹의 악몽이다.
남은 것은 단순하고 빠르며 내가 자주 직면하는 문제를 해결하는 패턴이다. 흥미로운 패턴은 아니다. 당신에게 영리함을 느끼게 하지도 않는다. 하지만 실제로 중요한 패턴이다.
최고의 정규 표현식 패턴은 6개월 후 오전 2시에 당신이 이해할 수 있는 패턴이다. 운영이 중단되고 사용자의 입력이 당신의 유효성 검사를 무너뜨리는 이유를 알아내려고 할 때 말이다.
정리에서 살아남은 20개의 패턴
다음은 내가 실제로 사용하는 정규 표현식 패턴의 전체 목록이다. 카테고리별로 정리되어 있다. 이들은 생존자들이다 - 실제 프로젝트의 반복 사용을 통해 그 가치를 입증한 패턴들이다.
| 패턴 | 사용 사례 | 빈도 | 노트 |
|---|---|---|---|
/^\s+|\s+$/g |
공백 제거 | 일일 | 네, 나는 .trim()이 존재한다는 것을 안다. 하지만 이것은 더 많은 컨텍스트에서 작동한다. |
/\s+/g |
공백 정규화 | 일일 | 여러 개의 공백을 단일 공백으로 대체 |
/[^a-z0-9]/gi |
특수 문자 제거 | 주간 | 슬러그, 사용자 이름 등을 위해 |
/^[a-z0-9_-]{3,16}$/i |
사용자 이름 검증 | 주간 | 알파벳, 밑줄, 하이픈, 3-16자 |
/^.{8,}$/ |
비밀번호 길이 | 주간 | 최소 8자 이상, 그게 전부 |
/.+@.+\..+/ |
이메일 검증 | 주간 | 99.9%의 경우에 충분하다 |
/^https?:\/\//i |
URL 프로토콜 확인 | 주간 | 그냥 http나 https, 화려한 것은 없다 |
/\d+/g |
숫자 추출 | 일일 | 간단하고 빠르다 |
/^\d+$/ |
숫자 입력 검증 | 주간 | 숫자만, 다른 것은 없음 |
/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/ |
날짜 형식 YYYY-MM-DD | 월간 | 형식 검사만, 검증은 아님 |
/^#?([a-f0-9]{6}|[a-f0-9]{3})$/i |
16진수 색상 코드 | 월간 | 해시가 있거나 없거나 |
/\$\{([^}]+)\}/g |
템플릿 변수 | 월간 | ${변수} 패턴 일치 |
//g |
HTML 주석 | 월간 | 주석 제거를 위해 |
/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/ |
IPv4 주소 | 월간 | 형식 검사, 범위 검증은 아님 |
/^[a-z0-9-]+$/i |
슬러그 검증 | 주간 | 소문자, 숫자, 하이픈만 |
/\r?\n/g |
줄 바꿈 | 주간 | 유닉스와 윈도우 모두 처리 |
/[<>]/g |
기본 |