RegEx(Regular Expressions, 정규 표현식)라는 용어를 들어본 적이 있을 것입니다. 처음에는 다소 어렵게 느껴질 수 있지만, 패턴의 의미와 실제 표현식을 구성하고 활용하는 방법에 익숙해지면 생각보다 어렵지 않습니다. 문자열과 텍스트를 좀 더 추상적인 관점에서 바라보는 사고에 익숙해지면, 데이터 집합에서 공통 패턴을 찾아야 하는 문제를 해결하는 데 강력한 도구가 됩니다.
RegEx는 패턴 매칭(pattern matching) 방식입니다. 즉, 특정 패턴을 기준으로 문자열이나 텍스트를 필터링하여 원하는 텍스트를 추출하거나 수정하는 방법입니다. 이 글에서는 정규 표현식을 사용하는 방법과, Ruby 메서드로 표현식을 테스트하여 프로젝트 로직에 적용하는 방법까지 살펴보겠습니다.
RegEx를 시각화하고 이해하는 데 큰 도움이 되는 도구 중 하나가 바로 Rubular라는 온라인 사이트입니다. 해당 사이트에 접속하면 미리 입력된 텍스트 블록으로 정규 표현식을 바로 테스트해 볼 수 있습니다. 두 개의 슬래시(/) 사이에 'neighbor'라는 단어가 포함된 문자열이 있는 것을 확인할 수 있습니다.
믿기 어렵겠지만, 이것이 바로 정규 표현식입니다! 단어 하나, 문장, 심지어 문단 전체도 기술적으로는 정규 표현식이라고 부를 수 있습니다(두 개의 슬래시 사이에 있기만 하면 됩니다). Rubular는 텍스트 블록에서 'neighbor' 패턴이 나타나는 모든 위치를 강조해서 보여줍니다. neighbor가 더 큰 단어의 일부인 경우까지 포함해서 말이죠! 하지만 정확한 단어 일치보다 더 유연하고 추상적인 패턴을 찾고 싶을 때가 있습니다. 바로 이때 메타문자(metacharacters)가 필요합니다.
메타문자(Metacharacters)
정규 표현식은 숙련된 프로그래머에게도 까다로울 수 있으니, 처음에 잘 안 되더라도 너무 낙담하지 마세요.
우리 주변의 거의 모든 것이 원자(atom)로 이루어져 있듯이, 메타문자는 정규 표현식의 빌딩 블록입니다. 정규 표현식에 요소를 추가할수록 전체 패턴이 달라지고, 패턴이 달라지면 메서드 실행 결과도 함께 달라집니다.
아래 표는 정규 표현식을 조정하여 자신에게 맞는 패턴을 찾는 다양한 방법을 정리한 것입니다. 전화번호나 이메일 등을 위한 정규 표현식에 절대적으로 '옳은' 작성 방법이란 없으며, 결국 프로젝트의 요구 사항에 따라 달라진다는 점을 기억하세요.
| 메타문자 | 설명 | 예시 |
|---|---|---|
| [abc] | a, b 또는 c 중 하나의 문자와 일치하는 문자 클래스 | /[eig]/는 neighbor, apple, gate의 일부와 일치 |
| [^abc] | a, b, c를 제외한 모든 문자와 일치하는 부정 문자 클래스 | /[^eig]/는 neighbor, apple, gate의 일부와 일치 |
| [a-z] | a-z 범위의 임의 한 문자와 일치하는 문자 클래스 | /[e-i]/는 neighbor, apple, gate의 일부에서 단일 문자와 일치 |
| [a-zA-Z] | a-z 또는 A-Z 범위의 문자와 일치하는 문자 클래스 | "Hi neighbor!", Grapple, gate 등에서 단일 문자와 일치 |
| ^ | 줄(line)의 시작 | /^Hello/는 'Hello'로 시작하는 줄과 일치 |
| $ | 줄의 끝 | /Goodbye$/는 'Goodbye'로 끝나는 줄과 일치 |
| \A | 문자열의 시작. '^'와 비슷하지만 멀티라인 모드가 적용되지 않음 | /\Aa/는 apple의 'a'와는 일치하지만, apricot의 'a'는 문자열 시작이 아니므로 일치하지 않음 |
| \z | 문자열의 끝. '$'와 비슷하지만 멀티라인 모드가 적용되지 않음 | /\za/는 zebra의 'a'와는 일치하지만, libra의 'a'는 문자열 끝이 아니므로 일치하지 않음 |
| . | 와일드카드. 점(.)은 임의의 문자 하나와 일치 | /./는 apple의 모든 단일 문자와 일치 |
| + | 바로 앞의 메타문자가 1개 이상 반복 | /aa+/는 'aa', 'aaaaaaa'와는 일치하지만 'a'와는 일치하지 않음(최소 1회 이상 필요) |
| * | 바로 앞의 메타문자가 0개 이상 반복 | /ab*/는 'a', 'ab', 'abbbbbb'와 일치 |
| \s | 공백 문자 | /^The\s.+s$/는 The Beatles, The Rolling Stones, The Cranberries 등과 일치 |
| \S | 공백이 아닌 문자 | /\S+/는 The Beatles, The Rolling Stones, The Cranberries 등과 일치 |
| \d | 임의의 숫자 | /\d+/는 22, 33333, 0 등과 일치 |
| \D | 숫자가 아닌 문자 | /\D+/는 'Hello, goodbye'와 일치 |
| \w | 단어 문자(영문자, 숫자, 밑줄) | /ny\w*/는 'ny_152', 'nypost39' 등과 일치 |
| \W | 단어 문자가 아닌 문자 | /\W+/는 ')(*&^%$'와 일치 |
| a{3} | 'a'가 정확히 3개 | /\d{3}-\d{3}-\d{4}/는 555-555-5555와 일치 |
| a{3} | 'a'가 3개 이상 | /[a-zA-Z0-9!#$^&*)(]{8}/는 'xE*BqRx14B7TAQp'와 일치 — 비밀번호로 쓸 만한 형태! |
| a{3,6} | 'a'가 3개 이상 6개 이하 | /[a-zA-Z0-9!#$^&*)(]{8,32}/는 '0XX!pC3Odpu30Qc'와 일치(길이가 8자 이상 32자 이하이므로) |
| a? | 'a'가 0개 또는 1개 | /\d?-\d{3}-\d{3}-\d{4}/는 국제 코드가 붙은 전화번호와 붙지 않은 전화번호 모두와 일치 |
메타문자는 웹사이트 양식(form)에 사용자가 입력하는 정보를 검증할 때 특히 유용합니다. 주소, 이메일, 전화번호의 형식이 올바른지 확인하는 데 RegEx를 활용하면, 새 계정 등록 시 발생하는 사용자 오류를 줄이고 더욱 체계적으로 정리된 데이터베이스를 유지할 수 있습니다.
Ruby에서 RegEx를 테스트하는 메서드
scan과 match의 차이를 확인하기 위해 사용할 코드는 다음과 같습니다.
#!/usr/bin/ruby
class RegexTest
def initialize(str, regex)
@str = str
@regex = regex
@result = str.scan(regex)
end
def display_details()
puts "String = #@str"
puts "regex = #@regex"
puts "result = #@result"
end
end
# 객체 생성
str1 = RegexTest.new("The rain in Spain stays mainly on the plain", /\w+ain/)
str2 = RegexTest.new("In Hertford, Hereford, and Hampshire, hurricanes hardly ever happen", /H\w+/)
# 메서드 호출
str1.display_details()
str2.display_details()
scan
Ruby의 scan 메서드는 정규 표현식과 일치하는 모든 문자열을 배열(array)로 반환합니다.
str1: result = ["rain", "Spain", "main", "plain"]
str2: result = ["Hertford", "Hereford", "Hampshire"]
반환된 배열을 원하는 방식으로 자유롭게 활용할 수 있습니다.
match
정규 표현식의 match 메서드는 scan과 매우 유사하지만, 모든 일치 항목이 아닌 첫 번째 일치 항목만 찾습니다. @result = str.scan(regex)를 @result = str.match(regex)로 변경하면 그 차이를 바로 확인할 수 있습니다.
str1: result = rain
str2: result = Hertford
단, match는 <MatchData> 객체를 반환한다는 점이 다릅니다. 이 객체에는 결과를 로직에 활용할 때 유용하게 쓸 수 있는 다양한 메서드가 포함되어 있으니, 자세한 내용은 Ruby 공식 문서를 참고하기 바랍니다.
grep
grep은 배열에서 일치하는 문자열을 찾는 열거형(enumerable) 메서드입니다. 정규 표현식과 일치하는 모든 문자열을 배열로 반환하며, 현재 코드 구조에서는 전달된 문자열을 먼저 배열로 분리해야 합니다.
이를 위해 다음 코드 줄을,
@result = str.match(regex)
아래와 같이 변경합니다.
@result = str.split(/\s|,/).grep(regex)
그러면 scan을 사용했을 때와 동일한 결과를 얻을 수 있습니다.
str1: result = ["rain", "Spain", "main", "plain"]
str2: result = ["Hertford", "Hereford", "Hampshire"]
=~ 연산자
=~ 기본 매칭 연산자를 사용하면 문자열과 정규 표현식을 비교하여 일치하는 첫 번째 인덱스(index)를 반환할 수 있습니다. 일치하는 항목이 없으면 nil을 반환합니다.
마무리
이 글에서는 Ruby에서 정규 표현식(RegEx)을 활용하는 방법을 살펴보았습니다. 메타문자의 의미를 익히고 scan, match, grep, =~ 연산자의 차이를 이해하면, 폼 검증부터 텍스트 추출까지 다양한 상황에서 RegEx를 자신 있게 사용할 수 있습니다. Ruby로 무엇을 만들 수 있는지 더 궁금하다면 관련 학습 자료를 추가로 참고해 보시기 바랍니다.