정규식으로 (한글) 문자만 골라내기

오늘 라라벨 꾸준 코딩 모임에서 나왔던 이야기인데, 라라벨의 유효성 검사 규칙 중 alpha에 관해 써본다.

원문에서는 아래와 같이 정의하고 있다.

The field under validation must be entirely alphabetic characters.

이를 정수님이 옮기시면서 역주를 추가하셨는데,

필드의 값이 완벽하게 (숫자나 기호가 아닌) 알파벳[자음과 모음] 문자로 이루어져야 합니다.
(역자주: 영문 알파벳만을 의미하지 않고, 숫자나 기호가 아닌경우에 해당하여, 한글도 허용합니다.)

여기서 자음과 모음이 언급되었길래 그럼 자음과 모음이 없는 중국어는 어떻게 되냐고 여쭤보았고,
(중국어에 진짜 자음과 모음이 없는가에 대한 문제는 일단 넘어가자..)

유니코드의 구간을 정해놓고 validation 처리한 예전 소스를 보여주신 분도 계셨다. 한글을 구분하는 코드였던가?
(좀 더 복잡한 소스였지만) 예를 들어 이런 식.

아래는 persian alphabet을 골라내는 정규식이(란)다.
(출처 : anetwork)

public function Alpha($attribute, $value, $parameters, $validator)
{
ValidationMessages::setCustomMessages( $validator );
$this->status = (bool) preg_match("/^[\x{600}-\x{6FF}\x{200c}\x{064b}\x{064d}\x{064c}\x{064e}\x{064f}\x{0650}\x{0651}\s]+$/u", $value);
return $this->status ;
}

결국 다같이 라라벨 소스를 보게 되었다.
막상 코드를 열어보니 단순해서 놀랐는데, 코드는 아래와 같다.

/**
* Validate that an attribute contains only alphabetic characters.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
protected function validateAlpha($attribute, $value)
{
return is_string($value) && preg_match('/^[\pL\pM]+$/u', $value);
}

“아니, 저건 무슨 정규식이죠?!”라며 놀랐지만, 내가 뚫어져라 본다고 알 턱이 없었다.

그래서 좀 찾아보기로…

Alphabet

내가 처음에 혼란스러웠던 것은, 유효성 규칙에 대한 설명에서 ‘알파벳[자음과 모음]’이라는 문자에서 과연 자음과 모음이라는 게 중요한가..라는 의문 때문이었다.

한국어 위키디피아의 음소문자 설명부터 보자.

음소문자(音素文字, alphabet)는 하나하나의 문자가 원칙적으로 하나의 자음 또는 모음의 음소(音素)를 나타내는 문자 체계이다. 자음과 모음에 대응하는 각각의 문자가 따로 존재하는게 다른 종류의 문자와의 다른 점이다. 로마 문자, 한글, 키릴 문자, 그리스 문자 같은 것들이 음소 문자 중 하나다.

위키디피아 설명에는 sets of letters used in written languages라는 설명으로 시작해서,

"alphabet" is a script that represents both vowels and consonants as letters equally라는 표현도 나온다.

자음과 모음을 동일하게 하나의 글자로써 표현하는 것인가 본데…

라라벨 매뉴얼에서 alphabet을 ‘알파벳[자음과 모음]’이라고 표현하신 건 이제 이해가 됐다.

Unicode property

유니코드 표준에서는 각 code point마다 여러가지 속성을 부여하는데,

이를 Unicode character property라고 하고, 이를 통해 Letter인지, Mark인지, Number인지 알 수 있다.

라라벨의 alpha가 정의하는 유효성 확인 정규식을 다시 보자.

/^[\pL\pM]+$/u

\p를 통해 property를 부여할 수 있고, 대문자로 쓰면 반대의 개념이다.

\pL은 문자(Letter)를 가져오고, \PL은 문자가 아닌 것만 가져온다.

\pM은 Mark를 의미하는데, 어디까지가 이 Mark에 부합하는가..가 좀 복잡하다.

이 중에는 악센트와 같이 옆에 있는 문자를 꾸며주는 역할을 하는 것도 있다. 그러니까 문자(이쯤되니 문자라는 표현이 맞는지도 모르겠다)를 꾸며주는 역할을 하는 걸 의미하는 듯 하다

더 자세하고 친절한 설명은 역시 stackoverflow에 있다.

그럼 ™ <- 요렇게 생긴 트레이드 ‘마크’는?

http://www.fileformat.info/info/unicode/char/2122/index.htm

UnicodeBlock이 LETTERLIKE_SYMBOLS로 정의되어 있다. 그렇다. 이런 걸 심볼이라 부른다.


마지막으로 저 정규식에는 u라는 변경자가 붙어있는데,

u (PCRE_UTF8)

이 변경자는 펄과 호환되지 않는 PCRE의 추가 기능을 사용하게 합니다. 패턴 문자열을 UTF-8으로 취급합니다. 유닉스에서는 PHP 4.1.0부터, win32에서는 PHP 4.2.3부터 사용할 수 있습니다. PHP 4.3.5부터 패턴의 UTF-8 유효성이 검사됩니다.

라고 PHP 매뉴얼에 나와있다.

이번에 매뉴얼 좀 찾아보다 발견했는데, 아사마루(?)님의 블로그에 이 정규식 부분이 한글화 + 보충 설명되어 있다.

PHP 정규식(PCRE)의 모든 것

validation#rule-alpha

비슷한 문제로 2014년에 라라벨 Github에 이슈가 등록됐다.

validator rules shouldn’t allow unicode

Taylor Otwell은 We can document it. 한마디하고 닫아버린다.
(어쩌자는 말인 지 모르겠다.)

ASCII에 존재하는 라틴 알파벳 만을 매칭하려는 게 아니라면 정규식 자체에는 문제가 없어보이지만,

여전히 alpha라는 단어를 쓰는 게 맞는 지는 잘 모르겠다.

Letter에 매칭되는 문자가 모두 알파벳에 속하지 않을테니까.

Run

$arr = [
"x", "A", "3", "\n", ".", "\t", "\r", "\f",
"주", "$", "я", "张", "@", "ⓐ", "à",
"\u{0300}", /* COMBINING GRAVE ACCENT */
];
preg_match_all('/\pL/u', implode('', $arr), $matchesL);
preg_match_all('/\pM/u', implode('', $arr), $matchesM);

print_r($matchesL);
print_r($matchesM);

결과가 예상 되시나요?


Array
(
[0] => Array
(
[0] => x
[1] => A
[2] => 주
[3] => я
[4] => 张
[5] => à
)

)
Array
(
[0] => Array
(
[0] => ̀
)

)

한글만 골라내기

이렇게 유니코드에는 수많은 카테고리가 존재하고, 이 중에는 역시 한글을 표현하는 카테고리도 존재한다.

preg_match_all('/\p{Hangul}/u', implode('', $arr), $matchesHangul);

print_r($matchesHangul);

결과가 예상 되시나요?

추가

페이스북에서 받은 질문을 좀 더 상세히 찾아보려는데,

역시 유니코드 이 동네는 까면 깔수록 깔 게 많다.

우선 질문은 “항상 /[가-힣]+/u 을 사용했는데 틀린 건가요?”였는데,

그 전에 문맥/비즈니스 요구 사항에 관해 반문해야할 게 좀 많다.

그 기능에서

  • 자모를 모두 사용한 완성된 글자만 필요한가?
  • 자모를 개별적으로 입력할 수 있어야 하는가?
  • 한국어에서 쓰이지 않는 글자 조합을 표시해야 하는가?
  • 입력한 자음이나 모음이 한글인지 검색해야 하는가?

유니코드에서 한글이 존재하는 구간은 다음과 같다.

Hangul Jamo (U+1100 to U+11FF)
Hangul Compatibility Jamo (U+3130 to U+318F)
Hangul Jamo Extended-A (U+A960 to U+A97F)
Hangul Syllables (U+AC00 to U+D7AF)
Hangul Jamo Extended-B (U+D7B0 to U+D7FF)

이중 /가-힣/으로 걸러낼 수 있는 건 한글 소리 마디 (Hangul Syllables)인데,
한글 자모 각각을 구별하기 위해선 Hangul Jamo (U+1100 to U+11FF)까지 살펴봐야 한다.

/ㄱ-힣/이라고 쓰는 사람도 많은데, ㄱㄴㄷ..ㅜㅠㅡㅣ까지의 구간과 가나다…힡힢힣까지의 구간 사이에 한자 등 다른 문자가 많이 포함되어 있으므로 금지해야 한다.
/ㄱ-ㅣ가-힣/으로 써야 한다.

페북에서는 옛한글(언급은 안했지만 아래아 같은 걸 염두에 뒀었다)이 현대 한글의 코드 구간 뒤쪽이라고 얘길 했었는데,
아래아의 경우는 한글 자모에 속해있다. 이외에 쓰이지 않는 많은 진짜(?) 옛한글은 호환용 한글 자모나 자모 확장 등에서 발견된다.

결론

올바른 정규식을 얻기 위해선 뭉뚱그려 ‘한글 문자를 검사하는 기능’ 정도로 설명하지 말고, 정확한 요구사항을 명시하는 게 좋다.

자모를 사용한 완성된 글자만 필요한가?

  • /가-힣/

자모를 개별적으로 입력할 수 있어야 하는가?

  • /ㄱ-ㅣ가-힣/

한국어에서 쓰이지 않는 옛한글이나 글자 조합도 표시해야 하는가?

  • /\p{Hangul}/u

입력한 자음이나 모음이 한글인지 검색해야 하는가?

  • 자음이나 모음을 하나만 입력하고 이게 속해있는 지 검색하는 기능이라면, ‘한글 호환 자모’에 관해서도 알아놔야 한다.

한글을 입력할 때 완성된 글자가 아니라 자음이나 모음 하나만 단독으로 입력하는 경우, 그 음소는 유니코드의 ‘한글 호환 자모(Hangul Compatibility Jamo)’ 영역에 있는 것을 사용하게 됩니다.

각자의 비즈니스 요구 사항에 따라 선택하면 될 일이지만,

향후의 요구 사항을 고려하는 좀 더 세심한 네이밍이 필요할 것 같다.

라라벨 유효성를 다시 한번 돌아보자.

메소드 명을 validateAlpha()라고 해놓고 /^[\pL\pM]+$/u로 검사하는 것은 올바른 선택인가?

thank to

http://blog.gaerae.com/2015/10/postgresql-hangul-regular-expression.html
http://www.programminginkorean.com/programming/hangul-in-unicode/

  • 뭐 이런 사이트가 다 있나 싶은데…이런 사이트도 있다

http://advent.perl.kr/2015/2015-12-04.html
http://d2.naver.com/helloworld/76650