
갑자기 이건 왜..?
백준 온라인 저지의 이 문제를 풀다가 알파벳 자음 검증을 날로 먹고 싶어서 찾아봤습니다.
1. 정공법
^[BCDFGHJKLMNPQRSTVWXYZ]$
^[B-DF-HJ-NP-TV-Z]$
저는 이 방법이 나쁘다고 생각하지 않습니다. 누가 봐도 이해 가능하거든요. 만약 팀원이 코드에 이렇게 적었다면 쓱 보고 별 생각 없이 이해할 수 있을 겁니다.
저보고 고르라고 하면 위쪽을 고르긴 할 거예요. 어차피 알파벳 노래 흥얼거리는 건 똑같은데 아래쪽은 어디서 멈춰야 하는지를 동시에 생각해야 해서 제 머리가 힘들어하거든요. 물론 둘 다 '멋짐'이 부족한 건 사실입니다. 그리고 알파벳 노래가 짧은 것도 아니잖아요? 그래서 저는 더 효율적인 방법은 없나 궁금했어요.
이건 안 되나요?
^[^AEIOU]$
가능은 합니다. 하지만 입력이 알파벳이 아니라 숫자나 공백 및 각종 특수문자가 포함된다면 이 문법만으로는 충분하지 않습니다. 일종의 특수해라고 생각해요.
2. lookahead
^(?![AEIOU])[A-Z]$
regex101에서 검색해봤더니 굉장히 특이한 게 나왔습니다. 솔직히 말하면 여태까지 저는 이론에서 말하는 정규 표현식(결정적 유한 상태 오토마타를 그릴 수 있는 친구들)만 쓸 줄 알았고, 이것만으로도 작은 문제들은 충분히 해결이 되어서 lookahead assertion 과 관련된 기능은 공부를 하지 않았습니다. 근데 이 해결법을 보니 공부해야겠더라구요.
negative lookahead assertion
(?=pattern)ㅁ
(?!pattern)ㅁ
MDN에서는 lookahead assertion을 다음과 같이 설명합니다.
A lookahead assertion "looks ahead": it attempts to match the subsequent input with the given pattern, but it does not consume any of the input — if the match is successful, the current position in the input stays the same.
여기서 크게 두 가지 특징을 볼 수 있는데요. 먼저 미래를 본다는 것입니다. 정규표현식을 검증할 때는 문자열의 시작 부분부터 검사합니다. 우리가 앞을 보고 걷는다는 걸 쓸데없이 어렵게 말한다면 미래에 도달할 장소를 보면서 걷는 것이라고 할 수 있는데요. lookahead assertion 에서 말하는 '앞' 역시 문자열 앞쪽을 말하는 게 아니라 앞으로 도달할 친구를 뜻합니다. 그래서 위쪽의 ㅁ
부분, lookahead 오른쪽에 있는 애들을 검사한다는 의미가 됩니다.
두 번째 특징은 입력을 소모하지 않는다는 것입니다. 일반적으로 정규표현식에서는 문자 하나를 '검사' 하면 그 문자는 통과(혹은 실패)하고 다음 문자를 보는데요(물론 실패하면 다음은 없습니다).
^[AEIOU][A-Z]$
이런 정규표현식을 생각해 보면, 먼저 첫 글자가 알파벳 대문자 모음이 맞는지 확인합니다. 맞다면 그 문자 하나는 넘어가고 "그 다음" 문자가 알파벳 대문자인지 확인합니다. 예를 들어 ID
를 검사한다고 합시다. 먼저 첫 번째 글자인 I
가 [AEIOU]
에 속하기 때문에 통과하고, 그 다음 글자인 D
는 그 다음인 [A-Z]
에 포함되므로 무사히 넘어갑니다. 검사 한 번에 한 글자씩이라는 규칙을 지키는 셈이죠.
^(?=[AEIOU])[A-Z]$
하지만 이런 경우는 다릅니다. 먼저 첫 글자가 알파벳 대문자 모음이 맞는지 확인합니다. 하지만 맞다고 해서 그 문자를 넘어가고 다음 문자를 보지 않습니다. 그냥 그 문자 그대로 다음 절차를 진행해요. 똑같이 ID
라는 문자열을 검사해 봅시다. 먼저 I
를 볼 텐데요. 일단 알파벳 대문자 모음이 맞으니 통과합니다. 하지만 그 다음 글자인 D
의 차례가 아니라, 여전히 I
의 차례를 유지합니다. I
역시 [A-Z]
에 속하니 통과합니다. 그러면 D
를 볼 차례인데요. 정규표현식에서 다음 검증할 친구는 $
, 문자열이 끝났는지입니다. 따라서 이 정규표현식으로 ID
를 검사하면 좋지 않은 결과가 나오는 것이죠. 검사는 두 번 했지만 소비한 건 한 글자입니다.
꼭 소비라는 단어로 이해할 필요는 없는 것 같아요. 그냥 const isTrue = isThisTrue && isThatTrue;
처럼 한 묶음으로 표현하기에는 좀 난감한 두 조건을 묶어주는 역할이라고 생각해도 될 것 같습니다.
^(?![AEIOU])[A-Z]$
우리의 원래 문제로 돌아와 봅시다. 이 정규표현식은 글자 하나에 대해서 다음과 같은 두 가지 검사를 수행합니다.
[AEIOU]
검사 실패 (lookahead에서=
는 통과를,!
는 실패를 의미합니다)[A-Z]
검사 통과
이 두 가지를 통과하면 남는 건 알파벳 대문자인 자음들 뿐이죠.
3. v-mode
/^[[A-Z]--[AEIOU]]$/v
맨 위쪽의 정공법을 생각해 보면, 자음 여부를 검사하기 위해서는 자음인지 보는 것보다 모음이 아닌지를 보는 게 훨씬 편하다는 생각이 들 거예요. 하지만 정규표현식에는 합집합과 차집합 개념이 없기 때문에 'A이면서 B인 것'이나 'A이지만 B는 아닌 것'과 같은 내용을 표현하기 쉽지 않았습니다. 그래서 집합 연산은 아니지만 lookup을 활용해서 일종의 흉내를 내서 해결책을 찾은 것이죠.
하지만 이제는 다릅니다. 최--신 기술인 v-mode를 사용하면 대괄호로 표현된 문자 집합끼리의 연산이 가능하거든요!
[[얘네들이면서]&&[동시에여기에도속하는것들]]
[[여기엔속하는데]--[여기에는없는애들]]
이렇게 사용할 수 있습니다. 반드시 v
flag를 붙여야 해요.
저는 이걸 알고 나서 기립박수를 치면서 백준에 답을 제출했는데 매몰차게 거절당하고 말았습니다. 그 이유는 바로..

작년(2023)에 나온 진짜 최신 기술이라서요. 심지어 ios는 작년 9월부터 지원하기 시작했기 때문에 하위호환성을 고려한다면 함부로 사용해서는 안 되는 기능입니다. 지금으로서는 정말 재미로만 알아야 하는 게 아닌가 싶네요.
그래서 당신은 뭘 썼죠?
이 문제에서는 ^[BCDFGHJKLMNPQRSTVWXYZ]$
이걸 썼습니다.
참고 자료
'호기심 천국' 카테고리의 다른 글
MVC (0) | 2024.04.27 |
---|---|
TanStack Query) staleTime 'Infinity' 는 어떻게 아는걸까? (0) | 2024.04.06 |
리액트와 대수적 효과는 무슨 관계일까? (1) | 2024.03.30 |
TypeScript) enum vs as const (2) | 2024.03.24 |
리액트 동시성이란 (4) | 2024.03.09 |