
정규 표현식(Regex): 문자열 패턴 찾기
이메일 주소가 맞는지 검사하려고 if문 100줄을 짤 것인가? 암호문 같은 기호들이 주는 강력함.

이메일 주소가 맞는지 검사하려고 if문 100줄을 짤 것인가? 암호문 같은 기호들이 주는 강력함.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", world## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", 는 "world로 끝나는 문자열"이다.
const startsWithHello = /^Hello/;
startsWithHello.test('Hello world'); // true
startsWithHello.test('Say Hello'); // false
const endsWithWorld = /world$/;
endsWithWorld.test('Hello world'); // true
endsWithWorld.test('world is big'); // false
나는 이걸 책의 첫 페이지와 마지막 페이지로 비유했다. ^는 표지, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", world## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", 는 "world로 끝나는 문자열"이다.
const startsWithHello = /^Hello/;
startsWithHello.test('Hello world'); // true
startsWithHello.test('Say Hello'); // false
const endsWithWorld = /world$/;
endsWithWorld.test('Hello world'); // true
endsWithWorld.test('world is big'); // false
나는 이걸 책의 첫 페이지와 마지막 페이지로 비유했다. ^는 표지, 는 뒷표지. 패턴이 정확히 어디에 있어야 하는지 고정하는 역할.
[abc]: a, b, c 중 하나[a-z]: 소문자 a부터 z까지 중 하나[0-9]: 숫자 0부터 9까지 중 하나[^abc]: a, b, c가 아닌 문자 (부정)대괄호 []는 "이 중에 하나"라는 의미다.
const vowel = /[aeiou]/;
vowel.test('apple'); // true
vowel.test('sky'); // true (y는 모음 아니지만 단어에 모음 포함)
const notDigit = /[^0-9]/;
notDigit.test('123'); // false (모두 숫자)
notDigit.test('12a3'); // true (a가 숫자 아님)
나는 이걸 자판기로 이해했다. [abc]는 "a, b, c 버튼 중 아무거나 눌러도 됨"이라는 뜻.
\d: 숫자 (Digit) = [0-9]\w: 단어 문자 (Word) = [a-zA-Z0-9_]\s: 공백 문자 (Space) = 스페이스, 탭, 줄바꿈 등\D: 숫자 아님 = [^0-9]\W: 단어 문자 아님\S: 공백 아님대문자는 소문자의 반대다. 이건 외우기 쉬웠다.
const hasNumber = /\d/;
hasNumber.test('abc123'); // true
const noSpaces = /^\S+$/;
noSpaces.test('hello'); // true
noSpaces.test('hello world'); // false (공백 있음)
*: 0개 이상+: 1개 이상?: 0개 또는 1개 (optional){n}: 정확히 n개{n,}: n개 이상{n,m}: n개 이상 m개 이하수량자는 앞의 패턴이 몇 번 반복되는지 지정한다.
const optionalS = /cats?/; // cat 또는 cats
optionalS.test('cat'); // true
optionalS.test('cats'); // true
optionalS.test('catttt'); // false
const phoneNumber = /\d{3}-\d{4}-\d{4}/; // 010-1234-5678 형식
phoneNumber.test('010-1234-5678'); // true
phoneNumber.test('010-123-5678'); // false (자릿수 안 맞음)
나는 수량자를 게임의 아이템 개수로 이해했다. "칼을 1개 이상 가져야 함(+)", "방패는 있어도 되고 없어도 됨(?)".
(): 그룹핑|: OR 조건괄호는 여러 문자를 묶어서 하나의 단위로 만든다. 파이프 |는 "또는"이다.
const catOrDog = /cat|dog/;
catOrDog.test('I have a cat'); // true
catOrDog.test('I have a dog'); // true
catOrDog.test('I have a bird'); // false
const repeatingGroup = /(ha)+/; // ha, haha, hahaha...
repeatingGroup.test('hahaha'); // true
repeatingGroup.test('haa'); // false (ha가 아니라 haa)
.: 줄바꿈 제외한 아무 문자나 하나점 하나가 조커 카드다.
const threeChars = /a.c/; // a + 아무거나 + c
threeChars.test('abc'); // true
threeChars.test('a9c'); // true
threeChars.test('ac'); // false (중간 문자 없음)
주의: 점 자체를 찾으려면 \.로 이스케이프해야 한다.
정규 표현식 뒤에 붙는 옵션들이 있다.
g: Global (모든 매칭 찾기, 첫 번째만 찾는 게 아니라)i: Ignore case (대소문자 무시)m: Multiline (여러 줄 모드, ^와 ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", world## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", 는 "world로 끝나는 문자열"이다.
const startsWithHello = /^Hello/;
startsWithHello.test('Hello world'); // true
startsWithHello.test('Say Hello'); // false
const endsWithWorld = /world$/;
endsWithWorld.test('Hello world'); // true
endsWithWorld.test('world is big'); // false
나는 이걸 책의 첫 페이지와 마지막 페이지로 비유했다. ^는 표지, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", world## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", 는 "world로 끝나는 문자열"이다.
const startsWithHello = /^Hello/;
startsWithHello.test('Hello world'); // true
startsWithHello.test('Say Hello'); // false
const endsWithWorld = /world$/;
endsWithWorld.test('Hello world'); // true
endsWithWorld.test('world is big'); // false
나는 이걸 책의 첫 페이지와 마지막 페이지로 비유했다. ^는 표지, 는 뒷표지. 패턴이 정확히 어디에 있어야 하는지 고정하는 역할.
[abc]: a, b, c 중 하나[a-z]: 소문자 a부터 z까지 중 하나[0-9]: 숫자 0부터 9까지 중 하나[^abc]: a, b, c가 아닌 문자 (부정)대괄호 []는 "이 중에 하나"라는 의미다.
const vowel = /[aeiou]/;
vowel.test('apple'); // true
vowel.test('sky'); // true (y는 모음 아니지만 단어에 모음 포함)
const notDigit = /[^0-9]/;
notDigit.test('123'); // false (모두 숫자)
notDigit.test('12a3'); // true (a가 숫자 아님)
나는 이걸 자판기로 이해했다. [abc]는 "a, b, c 버튼 중 아무거나 눌러도 됨"이라는 뜻.
\d: 숫자 (Digit) = [0-9]\w: 단어 문자 (Word) = [a-zA-Z0-9_]\s: 공백 문자 (Space) = 스페이스, 탭, 줄바꿈 등\D: 숫자 아님 = [^0-9]\W: 단어 문자 아님\S: 공백 아님대문자는 소문자의 반대다. 이건 외우기 쉬웠다.
const hasNumber = /\d/;
hasNumber.test('abc123'); // true
const noSpaces = /^\S+$/;
noSpaces.test('hello'); // true
noSpaces.test('hello world'); // false (공백 있음)
*: 0개 이상+: 1개 이상?: 0개 또는 1개 (optional){n}: 정확히 n개{n,}: n개 이상{n,m}: n개 이상 m개 이하수량자는 앞의 패턴이 몇 번 반복되는지 지정한다.
const optionalS = /cats?/; // cat 또는 cats
optionalS.test('cat'); // true
optionalS.test('cats'); // true
optionalS.test('catttt'); // false
const phoneNumber = /\d{3}-\d{4}-\d{4}/; // 010-1234-5678 형식
phoneNumber.test('010-1234-5678'); // true
phoneNumber.test('010-123-5678'); // false (자릿수 안 맞음)
나는 수량자를 게임의 아이템 개수로 이해했다. "칼을 1개 이상 가져야 함(+)", "방패는 있어도 되고 없어도 됨(?)".
(): 그룹핑|: OR 조건괄호는 여러 문자를 묶어서 하나의 단위로 만든다. 파이프 |는 "또는"이다.
const catOrDog = /cat|dog/;
catOrDog.test('I have a cat'); // true
catOrDog.test('I have a dog'); // true
catOrDog.test('I have a bird'); // false
const repeatingGroup = /(ha)+/; // ha, haha, hahaha...
repeatingGroup.test('hahaha'); // true
repeatingGroup.test('haa'); // false (ha가 아니라 haa)
.: 줄바꿈 제외한 아무 문자나 하나점 하나가 조커 카드다.
const threeChars = /a.c/; // a + 아무거나 + c
threeChars.test('abc'); // true
threeChars.test('a9c'); // true
threeChars.test('ac'); // false (중간 문자 없음)
주의: 점 자체를 찾으려면 \.로 이스케이프해야 한다.
정규 표현식 뒤에 붙는 옵션들이 있다.
g: Global (모든 매칭 찾기, 첫 번째만 찾는 게 아니라)i: Ignore case (대소문자 무시)m: Multiline (여러 줄 모드, ^와 가 각 줄마다 적용)s: Dotall (.이 줄바꿈도 포함)u: Unicodey: Stickyconst findAllDigits = /\d/g;
'a1b2c3'.match(findAllDigits); // ['1', '2', '3'] (모두 찾음)
const caseInsensitive = /hello/i;
caseInsensitive.test('HELLO'); // true
caseInsensitive.test('HeLLo'); // true
나는 g 플래그를 안 쓰다가 결과가 하나만 나와서 당황한 적이 있다. match() 쓸 때는 거의 항상 g 플래그가 필요했다.
이론만 알면 뭐하나. 실제로 써야 내 것이 된다. 내가 자주 쓰는 패턴들을 정리해본다.
const emailPattern = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
function isValidEmail(email) {
return emailPattern.test(email);
}
isValidEmail('user@example.com'); // true
isValidEmail('user.name@company.co.kr'); // true
isValidEmail('invalid@'); // false
isValidEmail('@invalid.com'); // false
분해해보면:
^[\w-\.]+: 시작부터 단어문자/하이픈/점이 1개 이상@: @ 기호 필수([\w-]+\.)+: 도메인 (단어-하이픈 + 점)이 1회 이상 반복[\w-]{2,4}## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", world## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", 는 "world로 끝나는 문자열"이다.
const startsWithHello = /^Hello/;
startsWithHello.test('Hello world'); // true
startsWithHello.test('Say Hello'); // false
const endsWithWorld = /world$/;
endsWithWorld.test('Hello world'); // true
endsWithWorld.test('world is big'); // false
나는 이걸 책의 첫 페이지와 마지막 페이지로 비유했다. ^는 표지, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", world## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", 는 "world로 끝나는 문자열"이다.
const startsWithHello = /^Hello/;
startsWithHello.test('Hello world'); // true
startsWithHello.test('Say Hello'); // false
const endsWithWorld = /world$/;
endsWithWorld.test('Hello world'); // true
endsWithWorld.test('world is big'); // false
나는 이걸 책의 첫 페이지와 마지막 페이지로 비유했다. ^는 표지, 는 뒷표지. 패턴이 정확히 어디에 있어야 하는지 고정하는 역할.
[abc]: a, b, c 중 하나[a-z]: 소문자 a부터 z까지 중 하나[0-9]: 숫자 0부터 9까지 중 하나[^abc]: a, b, c가 아닌 문자 (부정)대괄호 []는 "이 중에 하나"라는 의미다.
const vowel = /[aeiou]/;
vowel.test('apple'); // true
vowel.test('sky'); // true (y는 모음 아니지만 단어에 모음 포함)
const notDigit = /[^0-9]/;
notDigit.test('123'); // false (모두 숫자)
notDigit.test('12a3'); // true (a가 숫자 아님)
나는 이걸 자판기로 이해했다. [abc]는 "a, b, c 버튼 중 아무거나 눌러도 됨"이라는 뜻.
\d: 숫자 (Digit) = [0-9]\w: 단어 문자 (Word) = [a-zA-Z0-9_]\s: 공백 문자 (Space) = 스페이스, 탭, 줄바꿈 등\D: 숫자 아님 = [^0-9]\W: 단어 문자 아님\S: 공백 아님대문자는 소문자의 반대다. 이건 외우기 쉬웠다.
const hasNumber = /\d/;
hasNumber.test('abc123'); // true
const noSpaces = /^\S+$/;
noSpaces.test('hello'); // true
noSpaces.test('hello world'); // false (공백 있음)
*: 0개 이상+: 1개 이상?: 0개 또는 1개 (optional){n}: 정확히 n개{n,}: n개 이상{n,m}: n개 이상 m개 이하수량자는 앞의 패턴이 몇 번 반복되는지 지정한다.
const optionalS = /cats?/; // cat 또는 cats
optionalS.test('cat'); // true
optionalS.test('cats'); // true
optionalS.test('catttt'); // false
const phoneNumber = /\d{3}-\d{4}-\d{4}/; // 010-1234-5678 형식
phoneNumber.test('010-1234-5678'); // true
phoneNumber.test('010-123-5678'); // false (자릿수 안 맞음)
나는 수량자를 게임의 아이템 개수로 이해했다. "칼을 1개 이상 가져야 함(+)", "방패는 있어도 되고 없어도 됨(?)".
(): 그룹핑|: OR 조건괄호는 여러 문자를 묶어서 하나의 단위로 만든다. 파이프 |는 "또는"이다.
const catOrDog = /cat|dog/;
catOrDog.test('I have a cat'); // true
catOrDog.test('I have a dog'); // true
catOrDog.test('I have a bird'); // false
const repeatingGroup = /(ha)+/; // ha, haha, hahaha...
repeatingGroup.test('hahaha'); // true
repeatingGroup.test('haa'); // false (ha가 아니라 haa)
.: 줄바꿈 제외한 아무 문자나 하나점 하나가 조커 카드다.
const threeChars = /a.c/; // a + 아무거나 + c
threeChars.test('abc'); // true
threeChars.test('a9c'); // true
threeChars.test('ac'); // false (중간 문자 없음)
주의: 점 자체를 찾으려면 \.로 이스케이프해야 한다.
정규 표현식 뒤에 붙는 옵션들이 있다.
g: Global (모든 매칭 찾기, 첫 번째만 찾는 게 아니라)i: Ignore case (대소문자 무시)m: Multiline (여러 줄 모드, ^와 ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", world## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", 는 "world로 끝나는 문자열"이다.
const startsWithHello = /^Hello/;
startsWithHello.test('Hello world'); // true
startsWithHello.test('Say Hello'); // false
const endsWithWorld = /world$/;
endsWithWorld.test('Hello world'); // true
endsWithWorld.test('world is big'); // false
나는 이걸 책의 첫 페이지와 마지막 페이지로 비유했다. ^는 표지, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", world## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, ## 프롤로그 - ^[\w-.]+@([\w-]+.)+[\w-]## 프롤로그 - 이게 뭔데
처음 이 문자열을 봤을 때 나는 진심으로 당황했다. 개발자들이 농담으로 만든 암호문인 줄 알았다. "이게... 코드라고?" 키보드 위에서 고양이가 뛰놀다가 만든 결과물처럼 보였다. ^, , \w, +, ., []... 이 기호들이 대체 무슨 의미인지 전혀 감이 오지 않았다.
그런데 이게 이메일 주소 검증하는 코드란다. 나는 이메일 검증을 if문으로 짜려고 했었다. "@가 있나? 점이 있나? 공백은 없나?" 이런 식으로. 그런데 막상 짜보니 코드가 50줄이 넘어갔다. 그것도 엣지 케이스는 거의 다 놓치고 말이다.
정규 표현식(Regular Expression, Regex)은 문자열 패턴을 찾고 검증하는 미니 언어다. 처음엔 외계어처럼 보이지만, 이걸 이해하고 나니 문자열 처리가 완전히 달라졌다.
나는 처음에 정규 표현식을 거부했다. 너무 못생겼기 때문이다. 코드는 사람이 읽을 수 있어야 한다고 배웠는데, 정규 표현식은 그 원칙을 정면으로 위반하는 것처럼 보였다.
실제로 회사 코드베이스에서 이런 걸 본 적이 있다.
const urlPattern = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\�CB0�'\(\)\*\+,;=.]+$/;
이건 URL 검증하는 정규식이라고 했다. 나는 이 코드를 보고 "도대체 누가 이걸 유지보수하냐"고 투덜댔다. 6개월 뒤에 나 자신도 이해 못 할 코드 아닌가?
그래서 나는 정규 표현식 없이 문자열 처리를 하려고 했다. 전화번호에서 숫자만 추출하는 함수를 순수하게 for문과 if문으로 짰다.
function extractNumbers(phone) {
let result = '';
for (let i = 0; i < phone.length; i++) {
const char = phone[i];
if (char >= '0' && char <= '9') {
result += char;
}
}
return result;
}
동작은 한다. 그런데... 이게 최선일까? 나는 뭔가 바보 같은 일을 하고 있다는 느낌을 받았다.
전환점은 로그 파일을 파싱해야 하는 프로젝트였다. 서버 로그에서 IP 주소만 추출해야 했다. 로그 형식은 이랬다.
2025-04-29 10:23:45 [INFO] User login from 192.168.1.100
2025-04-29 10:24:12 [ERROR] Failed request from 10.0.0.55
2025-04-29 10:25:03 [INFO] API call from 172.16.0.1
나는 또 split()과 indexOf()로 노가다를 시작했다. 공백으로 자르고, "from" 다음 단어를 찾고... 코드가 점점 복잡해졌다. 그때 옆자리 선배가 다가와서 이렇게 말했다.
"그냥 정규식 쓰면 한 줄인데?"
const ipPattern = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
const ips = logContent.match(ipPattern);
끝이었다. 두 줄로 모든 IP 주소가 추출됐다. 나는 그 순간 깨달았다. 정규 표현식은 못생긴 게 아니라 "압축된" 거였다. 패턴을 표현하는 DSL(Domain Specific Language)이었다.
나는 이렇게 받아들이기로 했다. "정규 표현식은 문자열의 모양을 그림으로 그리는 언어다." \d는 숫자라는 그림, {1,3}는 "1개에서 3개까지"라는 수량, \.는 점이라는 기호. 이 그림들을 조합하면 "IP 주소"라는 패턴이 완성된다.
이 비유가 나한테는 완전히 와닿았다. 정규 표현식은 일반 코드가 아니라 "패턴 스케치"였다.
나는 정규 표현식을 언어로서 차근차근 배우기 시작했다. 먼저 알파벳부터.
^: 문자열의 시작이 두 개는 "위치"를 나타낸다. 예를 들어 ^Hello는 "Hello로 시작하는 문자열", 는 "world로 끝나는 문자열"이다.
const startsWithHello = /^Hello/;
startsWithHello.test('Hello world'); // true
startsWithHello.test('Say Hello'); // false
const endsWithWorld = /world$/;
endsWithWorld.test('Hello world'); // true
endsWithWorld.test('world is big'); // false
나는 이걸 책의 첫 페이지와 마지막 페이지로 비유했다. ^는 표지, 는 뒷표지. 패턴이 정확히 어디에 있어야 하는지 고정하는 역할.
[abc]: a, b, c 중 하나[a-z]: 소문자 a부터 z까지 중 하나[0-9]: 숫자 0부터 9까지 중 하나[^abc]: a, b, c가 아닌 문자 (부정)대괄호 []는 "이 중에 하나"라는 의미다.
const vowel = /[aeiou]/;
vowel.test('apple'); // true
vowel.test('sky'); // true (y는 모음 아니지만 단어에 모음 포함)
const notDigit = /[^0-9]/;
notDigit.test('123'); // false (모두 숫자)
notDigit.test('12a3'); // true (a가 숫자 아님)
나는 이걸 자판기로 이해했다. [abc]는 "a, b, c 버튼 중 아무거나 눌러도 됨"이라는 뜻.
\d: 숫자 (Digit) = [0-9]\w: 단어 문자 (Word) = [a-zA-Z0-9_]\s: 공백 문자 (Space) = 스페이스, 탭, 줄바꿈 등\D: 숫자 아님 = [^0-9]\W: 단어 문자 아님\S: 공백 아님대문자는 소문자의 반대다. 이건 외우기 쉬웠다.
const hasNumber = /\d/;
hasNumber.test('abc123'); // true
const noSpaces = /^\S+$/;
noSpaces.test('hello'); // true
noSpaces.test('hello world'); // false (공백 있음)
*: 0개 이상+: 1개 이상?: 0개 또는 1개 (optional){n}: 정확히 n개{n,}: n개 이상{n,m}: n개 이상 m개 이하수량자는 앞의 패턴이 몇 번 반복되는지 지정한다.
const optionalS = /cats?/; // cat 또는 cats
optionalS.test('cat'); // true
optionalS.test('cats'); // true
optionalS.test('catttt'); // false
const phoneNumber = /\d{3}-\d{4}-\d{4}/; // 010-1234-5678 형식
phoneNumber.test('010-1234-5678'); // true
phoneNumber.test('010-123-5678'); // false (자릿수 안 맞음)
나는 수량자를 게임의 아이템 개수로 이해했다. "칼을 1개 이상 가져야 함(+)", "방패는 있어도 되고 없어도 됨(?)".
(): 그룹핑|: OR 조건괄호는 여러 문자를 묶어서 하나의 단위로 만든다. 파이프 |는 "또는"이다.
const catOrDog = /cat|dog/;
catOrDog.test('I have a cat'); // true
catOrDog.test('I have a dog'); // true
catOrDog.test('I have a bird'); // false
const repeatingGroup = /(ha)+/; // ha, haha, hahaha...
repeatingGroup.test('hahaha'); // true
repeatingGroup.test('haa'); // false (ha가 아니라 haa)
.: 줄바꿈 제외한 아무 문자나 하나점 하나가 조커 카드다.
const threeChars = /a.c/; // a + 아무거나 + c
threeChars.test('abc'); // true
threeChars.test('a9c'); // true
threeChars.test('ac'); // false (중간 문자 없음)
주의: 점 자체를 찾으려면 \.로 이스케이프해야 한다.
정규 표현식 뒤에 붙는 옵션들이 있다.
g: Global (모든 매칭 찾기, 첫 번째만 찾는 게 아니라)i: Ignore case (대소문자 무시)m: Multiline (여러 줄 모드, ^와 가 각 줄마다 적용)s: Dotall (.이 줄바꿈도 포함)u: Unicodey: Stickyconst findAllDigits = /\d/g;
'a1b2c3'.match(findAllDigits); // ['1', '2', '3'] (모두 찾음)
const caseInsensitive = /hello/i;
caseInsensitive.test('HELLO'); // true
caseInsensitive.test('HeLLo'); // true
나는 g 플래그를 안 쓰다가 결과가 하나만 나와서 당황한 적이 있다. match() 쓸 때는 거의 항상 g 플래그가 필요했다.
이론만 알면 뭐하나. 실제로 써야 내 것이 된다. 내가 자주 쓰는 패턴들을 정리해본다.
const emailPattern = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
function isValidEmail(email) {
return emailPattern.test(email);
}
isValidEmail('user@example.com'); // true
isValidEmail('user.name@company.co.kr'); // true
isValidEmail('invalid@'); // false
isValidEmail('@invalid.com'); // false
분해해보면:
^[\w-\.]+: 시작부터 단어문자/하이픈/점이 1개 이상@: @ 기호 필수([\w-]+\.)+: 도메인 (단어-하이픈 + 점)이 1회 이상 반복완벽하진 않다. RFC 스펙을 100% 만족하려면 엄청 복잡해진다. 하지만 실제에선 이 정도면 충분했다.
const phonePattern = /\d{2,3}-\d{3,4}-\d{4}/;
function formatPhone(phone) {
// 숫자만 추출
const numbers = phone.replace(/\D/g, '');
// 010-1234-5678 형식으로 변환
if (numbers.length === 11) {
return numbers.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
} else if (numbers.length === 10) {
return numbers.replace(/(\d{2,3})(\d{3,4})(\d{4})/, '$1-$2-$3');
}
return phone; // 형식 안 맞으면 원본 반환
}
formatPhone('01012345678'); // '010-1234-5678'
formatPhone('010-1234-5678'); // '010-1234-5678'
formatPhone('10-1234-5678'); // '10-1234-5678' (지역번호)
replace()에서 $1, $2, $3는 괄호로 캡처한 그룹을 참조한다. 이걸 알고 나니 문자열 변환이 엄청 쉬워졌다.
마크다운 문서에서 링크 추출하기.
const urlPattern = /https?:\/\/[^\s]+/g;
function extractUrls(markdown) {
return markdown.match(urlPattern) || [];
}
const text = `
Check out https://example.com and http://test.org
Visit https://github.com/user/repo for more info.
`;
extractUrls(text);
// ['https://example.com', 'http://test.org', 'https://github.com/user/repo']
https?는 "http 또는 https" (s가 optional). [^\s]+는 "공백이 아닌 문자가 1개 이상".
function isStrongPassword(password) {
const minLength = /.{8,}/; // 최소 8글자
const hasUpper = /[A-Z]/; // 대문자 포함
const hasLower = /[a-z]/; // 소문자 포함
const hasNumber = /\d/; // 숫자 포함
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/; // 특수문자 포함
return (
minLength.test(password) &&
hasUpper.test(password) &&
hasLower.test(password) &&
hasNumber.test(password) &&
hasSpecial.test(password)
);
}
isStrongPassword('Pass123!'); // false (8글자 미만)
isStrongPassword('Password123!'); // true
isStrongPassword('password123!'); // false (대문자 없음)
복잡한 패턴 하나보다 간단한 패턴 여러 개로 쪼개니 가독성이 훨씬 좋았다.
이건 좀 어려웠다. "앞을 보거나 뒤를 보지만 실제로 소비하지는 않는" 패턴이다.
(?=...): Positive lookahead (뒤에 ... 가 있으면)(?!...): Negative lookahead (뒤에 ... 가 없으면)(?<=...): Positive lookbehind (앞에 ... 가 있으면)(?<!...): Negative lookbehind (앞에 ... 가 없으면)예를 들어, "달러 기호 뒤의 숫자만 추출"하고 싶다면:
const pricePattern = /(?<=\$)\d+/g;
'Item costs $100 and $250'.match(pricePattern); // ['100', '250']
(?<=\$)는 "앞에 $가 있는지 확인하지만 $는 포함하지 않음"이다.
비밀번호에서 "최소 8글자이면서 숫자 포함"을 한 패턴으로 표현:
const strongPassword = /^(?=.*\d).{8,}$/;
// (?=.*\d): 어딘가에 숫자가 있는지 확인 (lookahead)
// .{8,}: 실제로는 8글자 이상 매칭
솔직히 이 부분은 아직도 헷갈릴 때가 있다. 하지만 "조건 확인만 하고 커서는 안 움직임"이라고 이해하니까 조금 나아졌다.
정규 표현식의 가장 큰 문제는 가독성이다. "Write Once, Read Never"라는 말이 괜히 나온 게 아니다. 6개월 뒤 내가 짠 정규식을 보면 "이게 뭐지?" 싶다.
해결책:
// 좋은 예: 주석과 변수명으로 의도 명확히
const EMAIL_PATTERN = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/; // 기본 이메일 형식 검증
// 나쁜 예: 설명 없이 복잡한 패턴
const x = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i;
정규 표현식은 백트래킹 방식으로 매칭한다. 패턴이 복잡하고 입력이 길면 기하급수적으로 느려질 수 있다.
위험한 패턴:
const dangerous = /(a+)+b/;
// 이 패턴에 'aaaaaaaaaaaaaaaaaaaaac' 같은 입력을 주면
// 엔진이 'b'를 찾으려고 무한히 백트래킹함
(a+)+ 같은 중첩 수량자가 문제다. 각 a마다 "첫 번째 +에 포함? 두 번째 +에 포함?" 결정해야 하니까 조합이 폭발한다.
해결책:
HTML 파싱에 정규식 쓰지 마라. 진짜로. Stack Overflow에 유명한 답변이 있다:
"HTML을 정규식으로 파싱하려는 순간, 당신은 두 개의 문제를 갖게 됩니다."
HTML은 중첩 구조다. 정규식은 중첩을 제대로 처리 못 한다. DOM 파서나 라이브러리(cheerio, jsdom 등)를 써라.
또 다른 경우:
string.includes() 쓰는 게 낫다startsWith(), endsWith() 가 더 빠르다정규식 디버깅은 정말 힘들다. 나는 이 도구들을 쓴다.
match()와 exec() 콘솔 로그: 중간 결과 확인.const pattern = /(\d+)-(\d+)-(\d+)/;
const result = pattern.exec('2025-04-29');
console.log(result);
// [
// '2025-04-29', // 전체 매칭
// '2025', // 첫 번째 그룹
// '04', // 두 번째 그룹
// '29', // 세 번째 그룹
// index: 0,
// input: '2025-04-29',
// groups: undefined
// ]
정규 표현식은 강력하지만 양날의 검이다. 적재적소에 쓰면 코드가 간결해지고 생산성이 올라간다. 하지만 남용하면 유지보수 지옥이 된다.
나는 이렇게 정리했다:
처음엔 외계어 같았지만, 지금은 문자열 처리의 필수 도구가 됐다. 여전히 복잡한 패턴은 머리 아프지만, 그래도 if문 100줄보다는 낫다.
결국 정규 표현식은 "패턴 언어"다. 새로운 언어를 배우듯이 시간을 들여 익히면, 문자열이라는 세계에서 마법을 부릴 수 있다.