Ruby 정규 표현식(ruby regex)은 문자열 안에서 특정 패턴을 찾아내고, 그 결과를 추출해 추가 처리에 활용할 수 있게 해주는 강력한 도구입니다.
정규 표현식의 대표적인 활용 사례는 크게 두 가지입니다. 바로 유효성 검사(validation)와 파싱(parsing)입니다.
예를 들어, 이메일 주소를 생각해 봅시다. 정규 표현식을 사용하면 '올바른 이메일 주소의 형태'를 직접 정의할 수 있습니다. 즉, 프로그램이 유효한 이메일과 유효하지 않은 이메일을 스스로 구분할 수 있게 되는 것입니다.

Ruby에서 정규 표현식은 두 개의 슬래시(/) 사이에 정의합니다. 다른 문법 요소와 구분하기 위함이며, 가장 단순한 표현식은 하나의 단어 또는 한 글자만 매칭할 수도 있습니다.
예시:
# 'like'라는 단어 찾기 "Do you like cats?" =~ /like/
매칭에 성공하면 해당 단어가 처음 등장하는 인덱스를 반환하고, 실패하면 nil을 반환합니다. 인덱스가 필요 없다면 String#include? 메서드를 사용하는 것이 더 간단합니다.
문자열이 정규 표현식과 일치하는지 확인하는 또 다른 방법은 match 메서드를 사용하는 것입니다.
if "Do you like cats?".match(/like/) puts "매치 발견!" end
이제부터는 날짜, 전화번호, 이메일, URL 같은 것들을 매칭하고, 캡처하고, 치환할 수 있는 더 복잡한 패턴을 만드는 방법을 알아보겠습니다.
문자 클래스(Character Classes)
문자 클래스를 사용하면 매칭할 문자의 범위나 목록을 정의할 수 있습니다. 예를 들어 [aeiou]는 모든 모음과 일치합니다.
예제: 문자열에 모음이 포함되어 있는지 확인하기
def contains_vowel(str)
str =~ /[aeiou]/
end
contains_vowel("test") # 1 반환
contains_vowel("sky") # nil 반환
이 방법은 문자의 개수까지는 고려하지 않습니다. 개수를 다루는 방법은 곧 배우게 됩니다.
범위(Ranges)
범위를 사용하면 여러 글자나 숫자를 일일이 나열하지 않고도 매칭할 수 있습니다. 즉, [2-5]는 [2345]와 동일한 의미입니다.
유용하게 쓰이는 범위들:
- [0-9]: 0부터 9까지의 숫자와 일치
- [a-z]: a부터 z까지의 소문자와 일치
- [^a-z]: 부정(negated) 범위 — 소문자가 아닌 문자와 일치
예제: 문자열에 숫자가 포함되어 있는지 확인하기
def contains_number(str)
str =~ /[0-9]/
end
contains_number("The year is 2015") # 12 반환
contains_number("The cat is black") # nil 반환
기억하세요:
=~연산자의 반환값은 항상 인덱스 또는nil입니다.
문자 범위를 지정할 때 유용한 축약 문법도 있습니다.
- \w: [0-9a-zA-Z_]와 동일 (영숫자 + 언더스코어)
- \d: [0-9]와 동일 (숫자)
- \s: 공백 문자와 일치 (탭, 일반 공백, 줄바꿈)
각각의 부정 형태도 존재합니다.
- \W: [0-9a-zA-Z_]에 속하지 않는 모든 문자
- \D: 숫자가 아닌 모든 문자
- \S: 공백이 아닌 모든 문자
점(.) 문자는 줄바꿈을 제외한 모든 문자와 일치합니다. 리터럴 점 자체를 매칭하려면 반드시 이스케이프(escape)해야 합니다.
예제: 특수 문자 이스케이프하기
# 이스케이프하지 않으면 어떤 문자든 일치함 "5a5".match(/\d.\d/) # 이 경우에는 리터럴 점(.)만 일치함 "5a5".match(/\d\.\d/) # nil "5.5".match(/\d\.\d/) # 매치 성공
수량자(Modifiers)
지금까지는 한 번에 한 글자만 매칭할 수 있었습니다. 여러 문자를 매칭하려면 패턴 수량자를 사용해야 합니다.
| 수량자 | 설명 |
|---|---|
| + | 1개 이상 |
| * | 0개 이상 |
| ? | 0개 또는 1개 |
| {3,5} | 3개 이상 5개 이하 |
지금까지 배운 내용을 조합하면 더 복잡한 정규 표현식을 만들 수 있습니다.
예제: IP 주소처럼 보이는지 확인하기
# 참고: 이 패턴은 999.999.999.999처럼 잘못된 IP도 매칭할 수 있습니다.
# 하지만 여기서는 형식만 확인한다고 가정합니다.
def ip_address?(str)
# !!를 사용해 반환값을 불리언으로 변환
!!(str =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)
end
ip_address?("192.168.1.1") # true 반환
ip_address?("0000.0000") # false 반환
정확한 문자열 매칭
정확히 일치하는지 확인하려면 다른 종류의 앵커(anchor)가 필요합니다. 예시를 통해 살펴보겠습니다.
# 문자열이 정확히 네 글자인지 확인하고 싶지만,
# 아래 코드는 네 글자를 초과해도 매칭됩니다.
"Regex are cool".match /\w{4}/
# 대신 '줄 시작(^)'과 '줄 끝($)' 앵커를 사용합니다.
"Regex are cool".match /^\w{4}$/
# 이번에는 매칭되지 않습니다. 사실 .size로 길이를 확인하는 게 더 간단하지만,
# 개념을 이해하는 데는 충분한 예제입니다.
줄바꿈(\n) 이후의 각 줄이 아니라 문자열 전체의 시작과 끝을 엄격하게 매칭하려면 ^와 $ 대신 \A와 \Z(또는 \z)를 사용해야 합니다.
캡처 그룹(Capture Groups)
캡처 그룹을 사용하면 매칭 결과의 일부를 저장해 두었다가 나중에 재사용할 수 있습니다. 캡처하려는 부분을 괄호로 감싸면 됩니다.
예제: 로그 파일 파싱하기
Line = Struct.new(:time, :type, :msg)
LOG_FORMAT = /(\d{2}:\d{2}) (\w+) (.*)/
def parse_line(line)
line.match(LOG_FORMAT) { |m| Line.new(*m.captures) }
end
parse_line("12:41 INFO User has logged in.")
# 아래와 같은 객체가 생성됩니다:
#
이 예제에서는 =~ 대신 .match 메서드를 사용했습니다.
match 메서드는 매칭에 성공하면 MatchData 객체를, 실패하면 nil을 반환합니다. MatchData 클래스에는 유용한 메서드가 많으니 공식 문서를 참고해 보세요!
단순히 불리언 값(true/false)만 필요하다면 Ruby 2.4부터 제공되는 match? 메서드를 사용할 수 있습니다. MatchData 객체를 생성하지 않으므로 match보다 더 빠르다는 장점도 있습니다.
캡처된 데이터는 .captures 메서드로 접근하거나, MatchData 객체를 배열처럼 다루면 됩니다. 인덱스 0에는 전체 매칭 결과가 들어가고, 이후 인덱스에는 각 캡처 그룹의 결과가 순서대로 저장됩니다.
첫 번째 캡처 그룹에 접근하려면 다음과 같이 하면 됩니다.
m = "John 31".match /\w+ (\d+)/ m[1] # "31"
캡처하지 않는 그룹(non-capturing group)도 사용할 수 있습니다. 표현식을 묶기만 할 뿐 저장하지 않으므로 성능상 이점이 있습니다. 또한 이름 있는 그룹(named group)을 사용하면 복잡한 표현식을 훨씬 읽기 쉽게 만들 수 있습니다.
| 문법 | 설명 |
|---|---|
(?:...) |
캡처하지 않는 그룹 |
(?<foo>...) |
이름 있는 그룹 |
예제: 이름 있는 그룹 사용하기
m = "David 30".match /(?<name>\w+) (?<age>\d+)/ m[:age] # => "30" m[:name] # => "David"
이름 있는 그룹 역시 MatchData 객체를 반환하며, 이 객체를 통해 결과를 읽을 수 있습니다.
전방 탐색과 후방 탐색(Look Ahead & Look Behind)
이것은 좀 더 고급 기법으로, 모든 정규 표현식 엔진에서 지원하지는 않습니다. 다행히 Ruby의 정규 표현식 엔진은 이를 지원하므로, 활용 방법을 살펴보겠습니다.
탐색(lookaround)을 사용하면 특정 패턴 앞이나 뒤에 무엇이 있는지 '몰래 들여다볼' 수 있습니다.
| 이름 | 설명 |
|---|---|
| (?=pat) | 긍정형 전방 탐색(positive lookahead) |
| (?<=pat) | 긍정형 후방 탐색(positive lookbehind) |
| (?!pat) | 부정형 전방 탐색(negative lookahead) |
| (?<!pat) | 부정형 후방 탐색(negative lookbehind) |
예제: 최소 한 글자 이상의 문자 뒤에 숫자가 오는지 확인하기
def number_after_word?(str)
!!(str =~ /(?<=\w) (\d+)/)
end
number_after_word?("Grade 99")
Ruby의 Regexp 클래스
Ruby의 정규 표현식은 Regexp 클래스의 인스턴스입니다. 대부분의 경우 이 클래스를 직접 다룰 일은 없지만, 알아두면 좋습니다.
puts /a/.class # Regexp
활용 예로, 문자열로부터 정규 표현식을 생성할 수 있습니다.
regexp = Regexp.new("a")
정규 표현식을 만드는 또 다른 방법은 %r 표기법을 사용하는 것입니다.
regexp = %r{\w+}
정규 표현식 옵션
정규 표현식에 옵션을 설정하면 동작 방식을 바꿀 수 있습니다.
| 옵션 | 설명 |
|---|---|
| i | 대소문자 구분 없이 매칭(case insensitive) |
| m | 점(.)이 줄바꿈 문자도 매칭 |
| x | 공백 무시 (가독성 향상) |
옵션을 사용하려면 정규 표현식을 닫는 / 뒤에 해당 글자를 붙이면 됩니다.
사용 예:
"abc".match?(/[A-Z]/i)
긴 정규 표현식 보기 좋게 작성하기
복잡한 Ruby 정규 표현식은 읽기 어려워질 수 있습니다. 이럴 때 'x' 옵션을 사용하면 표현식을 여러 줄로 나눌 수 있고, 심지어 정규 표현식 안에 주석도 작성할 수 있습니다.
예제:
LOG_FORMAT = %r{
(\d{2}:\d{2}) # 시간
\s(\w+) # 이벤트 타입
\s(.*) # 메시지
}x
Ruby 정규 표현식 종합 활용
정규 표현식은 다양한 Ruby 메서드와 함께 사용할 수 있습니다.
- .split
- .scan
- .gsub
- 그 외 많은 메서드들...
예제: .scan으로 문자열에서 모든 단어 추출하기
"this is some string".scan(/\w+/) # => ["this", "is", "some", "string"]
예제: 문자열에서 모든 숫자 추출하기
"The year was 1492.".scan(/\d+/) # => ["1492"]
예제: 문자열의 모든 단어 첫 글자 대문자로 변환하기
str = "lord of the rings"
str.gsub(/\w+/) { |w| w.capitalize }
# => "Lord Of The Rings"
예제: 이메일 주소 유효성 검사하기
email = "test@example.com" !!email.match(/\A[\w.+-]+@\w+\.\w+\z/) # true
마지막 예제에서는 !!를 사용해 결과를 불리언 값(true/false)으로 변환했습니다. Ruby 2.4 이상이라면 이 변환을 자동으로 처리해주고 더 빠른 match? 메서드를 사용하는 것도 좋은 선택입니다.
마무리
정규 표현식은 놀라울 정도로 강력하지만, 때로는 까다롭게 느껴질 수 있습니다. rubular.com 같은 도구를 활용하면 Ruby 정규 표현식을 더욱 인터랙티브하게 만들고 테스트할 수 있습니다. Rubular에는 유용한 Ruby 정규 표현식 치트 시트도 함께 제공되니 꼭 확인해 보세요. 이제 에디터를 열고 직접 코딩을 시작해 볼 차례입니다!
아, 그리고 이 글이 도움이 되었다면 주변 친구들에게 공유해서 더 많은 사람들이 함께 배울 수 있도록 해주세요.