화요정규식 – 2주차 : 회색조 색상

2주차 문제는 CSS로 표현할 수 있는 회색조 색상을 검증하는 정규식을 만드는 것입니다. 규칙 자체는 이해하기 어렵지 않은데 정규식으로 가질 수 있는 숫자의 범위를 한정 짓는 부분이 생각보다 귀찮습니다. 쓰다보면 정규식이 심하게 지저분해지는 것처럼 보여서 이걸 왜 정규식으로 해야하나하는 생각까지 들더군요. ㅠ_ㅠ

예를 들어, 0부터 255까지의 값을 가질 수 있는 숫자가 있다고 하면 보통은 숫자 1개 이상을 뜻하는 \d+를 쓰거나 혹은 숫자 1개부터 3개까지를 쓸 수 있는 \d{1,3}를 사용합니다. 물론, 전자는 123858283 후자는 999와 같은 값도 가능해지기 때문에 정확한 표현은 아닙니다. 하지만 일반적으로는 그리 사용해도 문제가 없는 수준일 때가 많고, 숫자값의 범위는 정규식 서브 패턴으로 숫자를 추출한 후 프로그래밍 코드에서 체크하는 것이 훨씬 간편하고 이해도 쉬워 그리 사용하는 편이 좋습니다. 가뜩이나 외계어같은 기호가 가득한 정규식인데 굳이 범위를 체크하겠다고 프로그래밍 코드가 할 일을 정규식에 넘길 필요는 없다고 생각합니다.

그런데, 이번 문제에서는 값의 범위까지 요구하고 나서서 정규식이 길고 지저분해졌습니다. ㅠ_ㅠ 예를 들어 앞서 말한 0부터 100까지의 숫자를 정규식으로 표현하면 다음과 같습니다.

25[0-5]|\d{1,2}|1\d{2}|2[0-4]\d

그런데 사실 숫자값은 0또는 양의 값을 가지는 실수(floating number)입니다. 앞서 작성한 수식에 몇 가지 규칙(숫자 앞에는 0이 올 수 있다, 소수점 앞의 0은 생략 가능하다 등)이 더해지면 겨우 숫자 하나를 체크하는데 이만큼의 정규표현식이 필요해집니다.

0*(25[0-5]|(\d{1,2}|1\d{2}|2[0-4]\d)(\.\d+)?|\.\d+)

개인적으로는 이게 단지 한 자리일 뿐이라는 것이 이번 과제의 재앙이었다고 생각합니다(왜 그런지는 결과를 보시면 압니다). 2주차 과제는 다음과 같이 일부 문자열은 정규식에 매칭되어야 하고 또 일부 문자열은 올바르지 않은 형식이므로 정규식에 매칭되지 않아야 합니다.

매칭되어야 하는 문자열

#000
#aaa
#eEe
#111111
#6F6F6F
#efEfEF
rgb(0, 0, 0)
rgb(15,15,15)
rgb(2.5, 2.5,2.5)
rgb(1, 01, 000001)
rgb(20%, 20%,20%)
rgba(4,4,4,0.8)
rgba(4,4,  4,1 )
rgba(3,3,3,0.12536)
rgba(10%,10%,10%,5%)
hsl(20,0%,  50%)
hsl(0, 10%, 100%)
hsl(0.5, 10.5%, 0%)
hsl(5, 5%, 0%)
hsla(20, 0%, 50%, 0.88)
hsla(0, 0%, 0%, 0.25)

매칭되지 않아야 할 문자열

#ef4
#eEf
#11111e
#123456
rgb(2, 4, 7)
rgb(10, 10,100)
rgb(1.5%, 1.5%, 1.6%)
rgba(1, 01, 0010, 0.5)
hsl(20, 20%, 20%)
hsl(0, 1%, 01%)
hsla(0, 10%, 50%, 0.5)
#11111
#000000000
rgb (1, 1, 1)
rgb(10, 10, 10, 10)
rgb(257, 257, 257)
rgb(10%, 10, 10)
hsl (20,0%,  50%)
argb(1,1,1)

CSS 색상 체계

이 과제를 수행하려면 먼저 CSS에서 표현되는 회색조 색상(grayscale colors)에 대해 이해할 필요가 있습니다. 사전적인 정의에 따르면 회색조 색상은 오로지 검은색과 흰색의 조합만으로 나타날 수 있는 색상을 말합니다. CSS에서 색상을 표현하는 방법은 크게 3가지(알파채널까지 추가하면 5가지)인데, 각 방식에서 회색조 색상을 표현하려면 다음과 같은 규칙을 따라야 합니다.

1. 16진수 체계
샾(#)을 제일 앞에 붙이고 빛의 삼원색인 R(red), G(green), B(blue) 각 두 자리의 16진수를 순서대로 표현하는 방식입니다. 예를 들어, 빨간색은 #FF0000으로 표현할 수 있습니다. 이 체계는 다음과 같은 규칙을 따릅니다.

  • 대소문자를 구분하지 않는다.
  • R, G, B는 각 2자리의 16진수를 사용한다. 16진수 한 자리의 범위는 0부터 F까지이다.
  • 만약 #RRGGBB와 같이 코드가 반복되는 형태라면 #RGB로 줄일 수 있다. 예를 들어, #335566은 #356으로 줄 일 수 있다.
  • R, G, B의 값이 모두 서로 같으면 회색조 색상이다. 예) #FFFFFF, #555555, #000 등

2. RGB 체계
rgb(R, G, B)와 같은 형식으로 색을 표현합니다.

  • 대소문자를 구분하지 않는다
  • R, G, B 값은 0부터 255까지의 실수 또는 0%부터 100%까지 실수 형태의 퍼센트로 표현할 수 있다.
  • R, G, B 값이 모두 서로 같으면 회색조 색상이다.
  • 숫자 앞에는 0이 몇 개든 올 수 있으며 없을 수도 있다.
  • 소수점 앞의 0은 생략할 수 있다. 예) .5231
  • 각 값은 쉼표(,)로 구분한다.
  • 공백은 무시한다. 단, 값에는 공백이 포함될 수 없다.

RGB 체계에 투명도를 나타내는 알파(alpha) 채널을 추가하면 rgba(R, G, B, A)와 같은 형식으로 색을 표현하는 RGBA 체계가 됩니다. RGBA 체계는 RGB 체계에 더해 다음 규칙을 따릅니다.

  • 알파 채널의 값은 0부터 1까지의 실수 또는 0% 부터 100%까지 실수 형태의 퍼센트로 표현할 수 있다.

3. HSL 체계
hsl(H, S, L)와 같은 형식으로 색을 표현합니다. H(hue), S(saturation), L(lightness)는 각각 색조, 채도, 밝기를 뜻합니다.

  • 대소문자를 구분하지 않는다
  • 색조는 0부터 360까지의 실수로 표현한다.
  • 채도, 밝기는 0%부터 100%까지의 실수로 표현한다.
  • 채도가 0%이거나 밝기가 0% 또는 100%이면 회색조 색상이다.
  • 숫자 앞에는 0이 몇 개든 올 수 있으며 없을 수도 있다.
  • 소수점 앞의 0은 생략할 수 있다. 예) .5231
  • 각 값은 쉼표(,)로 구분한다.
  • 공백은 무시한다. 단, 값에는 공백이 포함될 수 없다.

HSL 역시 알파 채널을 추가한 HSLA 체계가 있는데, 알파 채널의 규칙은 RGB 체계와 같습니다.

  • 알파 채널의 값은 0부터 1까지의 실수 또는 0% 부터 100%까지 실수 형태의 퍼센트로 표현할 수 있다.

정규식 문법 힌트

이 문제를 해결할 때 유용한 정규식 문법은 다음과 같습니다.

{n,m} 수량자

수량자(quantifier) 바로 앞의 문자 또는 그룹이 n개부터 m개까지 있음을 뜻합니다. 예를 들어, a{3,5}라는 패턴은 “aaa”, “aaaa”, “aaaaa”에 일치합니다. 또한 (ab){1,2}라는 패턴은 “ab”, “abab”에 일치합니다. {n,}처럼 m을 생략하면 n개 이상 있음을 의미합니다.

? 수량자

수량자 바로 앞의 문자 또는 그룹이 없거나 1개 있음을 뜻합니다. {0,1}와 같은 의미입니다.

* 수량자

수량자 바로 앞의 문자 또는 그룹이 없거나 1개 이상 있음을 뜻합니다. {0,}와 같은 의미입니다.

+ 수량자

수량자 바로 앞의 문자 또는 그룹이 1개 이상 있음을 뜻합니다. {1,}와 같은 의미입니다.

OR 연산자

A 패턴 또는 B 패턴을 표현할 때는 파이프 문자(|)로 A, B 패턴을 서로 연결합니다. 예를 들어, abcxyz 또는 defxyz를 찾고 싶다면 /(abc|def)xyz/ 로 정규식을 작성하면 됩니다. 몇 개의 패턴이든 이어 붙일 수 있습니다.

문자열 시작과 끝

문자열 시작은 ^로, 끝은 $로 표현합니다. 예를 들어, /^a/ 라는 패턴은 “a”로 시작하는 문자열에 일치하므로 “abc”, “adef” 등에는 일치하나 “baaaa”에는 일치하지 않습니다. 마찬가지로 문자열의 끝은 $에 일치하므로 /^a(bcd|efg)$/라는 패턴은 정확히 “abcd”, “aefg” 문자열에만 일치하고 “cabcd” 또는 “aefgh” 등에는 일치하지 않습니다.

일치 문자

[abc]과 같은 형식으로 일치 문자를 정할 수 있습니다. 예를 들어, /[abc]d/는 “ad”, “bd”, “cd” 에 일치합니다. 또한 [시작문자-끝문자]와 같이 일치할 문자의 범위를 정할 수도 있습니다. 예를 들어, /[a-d]z/는 “a부터 d까지의 문자 하나와 z문자” 즉, “az”, “bz”, “cz”, “dz”에 일치합니다. 시작과 끝 범위는 해당 문자가 가지는 문자 코드를 기준으로 합니다. 따라서 [A-z]라고 하면 알파벳 이외의 문자가 포함됩니다(Z의 문자 코드는 90이지만 a는 97입니다). 알파벳만 선택하고 싶다면 [A-Za-z]라고 작성하는 것이 옳습니다.

이스케이프 문자

정규식 안에서 백 슬래시(\)를 앞에 붙인 문자들은 모두 정규식에서 특수한 의미를 가집니다. 혹은 원래 특수한 의미로 사용되던 문자를 문자 그대로 표현할 때도 사용합니다. 예를 들어, 점(.)은 정규식에서 ‘모든 문자’라는 의미를 가지고 있습니다. 하지만 \.처럼 사용하면 문자 그대로 ‘.’ 문자에만 일치합니다. 마찬가지로 괄호 문자 – ‘(‘ 또는 ‘)’ – 역시 원래는 서브 패턴을 만드는 특수한 용도로 사용되지만 \(이나 \)처럼 사용하면 문자 그대로 각각 여닫는 괄호 문자에 일치합니다.

숫자

이스케이프 문자 \d는 ‘숫자 하나’를 의미합니다. [0-9]와 같은 의미입니다.

공백

이스케이프 문자 \s (소문자 s)는 ‘공백 한 문자’를 의미합니다. 다시 말해, 줄바꿈(\n), 라인피드(\r), 탭(\t), 스페이스( ) 한 문자에 일치합니다.

해답 및 풀이

앞서 말했듯이 개인적으로 정규식으로 범위를 체크해야 하는 경우는 별로 없다고 생각합니다. 범위를 집어넣는 순간 가뜩이나 복잡한 정규식이 정말 이해 불가능한 외계어로 변해 작성자조차 알아보기 힘들게 되기 십상입니다. 그래서 제 해답은 문제를 통과할 수 있는 최소한의 수준에만 맞춰 정규식을 작성했습니다. 만약 정규식만으로 문자열을 더 엄격하게 체크하고 싶다면 보강을 하셔야 한다는 뜻이니 이 점 주의하시기 바랍니다.

언제나 그렇듯 제가 내는 해답은 “한가지 의견”일 뿐입니다. 충분히 다른 답도 가능하며, 어쩌면 저보다 훨씬 좋은 답도 나올 수 있습니다. 그러니 꼭 먼저 풀어보신 후에 제 해답은 참고만 해주셨으면 좋겠습니다. 🙂

[toggle txt_show=”2주차 풀이 보기” txt_hide=”2주차 풀이 감추기”]해답
/^#([\da-f]{1,2})\1\1$|^rgb\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\s*0*\2){2}\)$|^rgba\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\s*0*\6){2},\s*\d{0,3}(\.\d+)?%?\s*\)$|^hsl\([.\d]+,\s*(0%,\s*[\.\d]+%|[\.\d]+%,\s*(0|100)%)\)$|^hsla\([.\d]+,\s*(0%,\s*[\.\d]+%|[\.\d]+%,\s*(0|100)%),\s*[\.\d]+\)$/i

풀이
이 문제에는 서로 확연히 다른 세 가지 패턴(#RGB, rgb(…), hsl(…))이 있고, 거기서 파생된 두 가지 패턴(rgba, hsla)이 있습니다. 우선 세 가지 패턴을 각각 작성한 다음 OR 연산자를 사용해 세 패턴을 붙이는 편이 편리합니다.

1. #RGB 패턴
항상 샾이 먼저 나와야 하므로 이 패턴은 /^#...$/ 와 같이 작성될 것입니다. 굳이 문자열 처음부터 끝까지 일치해야 한다는 규칙을 추가한 이유는 #112233은 올바른 형식이지만 #1122334는 올바르지 않은 패턴이므로 이를 걸러낼 수 있어야 하기 때문입니다. 즉, 문자열 끝까지 정확하게 일치해야 합니다. 16진수의 범위는 [0-9a-f]와 같이 작성될 수 있으므로 이 패턴은 다음과 같이 작성될 수 있습니다. 단, 숫자 [0-9]는 코드를 더 작게 만들기 위해 이스케이프 문자로 대체했습니다.

/^#[\da-f]{6}$/

하지만 우리가 원하는 회색조를 구하려면 RGB의 값이 모두 같아야 합니다. 이를 위해서는 먼저 R의 패턴에 일치하는 값을 찾아서 G, B에 같은 값을 적용하면 됩니다.

/^#([\da-f]{2})\1\1$/

‘\1’과 같은 백 레퍼런스(back reference)는 1주차에 사용되었던 개념이라 따로 설명하지 않겠습니다. 또한 #RRGGBB는 #RGB와 같이 표현될 수 있고, 대소문자를 구분하지 않으므로 #RGB 패턴 회색조는 다음과 같이 표현할 수 있습니다.

/^#([\da-f]{1,2})\1\1$/i

문자열 끝 문자($)를 사용했을 때와 그렇지 않을 때, i 변경자를 사용했을 때와 그렇지 않을 때 결과가 어떻게 달라지는지도 비교해보세요.

2. rgb() 패턴
정의에 의해 이 패턴은 /^rgb\(R,G,B\)$/와 같이 작성합니다. 문제를 보면 쉼표 뒤쪽에 1개 이상의 공백이 있는 패턴도 있으므로 정확히는 /^rgb\(R,\s*G,\s*B\)$/와 같을 것입니다(조금 더 정확하게 하려면 괄호 주변, 쉼표 앞에도 공백 패턴을 포함시켜야 하지만 여기서는 생략했습니다).

R값을 작성하기 위해 숫자 패턴을 만들어보겠습니다. 먼저 0부터 255까지의 자연수는 다음과 같이 표현할 수 있습니다.

25[0-5]|2[0-4]\d|\d{1,2}|1\d{2}

이 패턴에 “실수”라는 조건을 추가해보겠습니다. 실수는 0부터 254까지의 자연수 뒤에 추가될 수 있으므로 다음과 같이 숫자 패턴을 바꿀 수 있습니다.

25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?

여기에 “0%부터 100%까지의 실수”라는 조건을 추가합니다. 원래는 다음과 같이 0%부터 100%까지라는 규칙을 작성한 후 이를 앞의 패턴에 추가해야 할 것입니다.

(100|\d{1,2}(\.\d+)?)%

하지만 다행히(?) 문제에서는 100%를 넘는 패턴은 고려하지 않아도 되므로 그냥 숫자 패턴의 뒤에 “%가 있을 수도 있다” 정도만 추가하겠습니다.

25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?

이제 R값 패턴을 다 구했습니다. 앞서 #RGB와 마찬가지로 RGB의 값이 모두 같게 만들어주면 됩니다.

/^rgb\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\1){2}\)$/

하지만 문제를 보면 쉼표 뒤에 공백이 있을 수도 있습니다.

/^rgb\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\s*\1){2}\)$/

또한 숫자 앞에 여러 개의 0이 있을 수도 있습니다.

/^rgb\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\s*0*\1){2}\)$/

2-1. rgba() 패턴
PCRE 계열 정규식이 지원하는 기능 중 조건부 일치라는 기능이 있습니다. 특정 조건을 만족시켰을 때와 그렇지 않았을 때 사용하는 패턴이 달라질 수 있습니다. 정규식 문법은 (?(조건)조건에 일치했을 때|그렇지 않을때)와 같습니다. 예를 들어, /a(b)?(?(1)xyz|123)/과 같이 작성하면 사실 /a(bxyz|123)/과 비슷한 의미입니다. 즉, “abxyz”, “a123″에 일치합니다. 서브 패턴으로 묶은 “b”의 일치 여부에 따라 사용할 패턴을 달리 할 수 있다는 뜻입니다. (?(1)xyz|123)에서 (1)은 ‘첫 번째 서브 패턴’을 뜻합니다. 서브 패턴을 사용하는 대신 (?=regex)와 같이 직접 패턴을 사용할 수도 있습니다. 예컨대 앞의 예제는 /a(?(?=b)xyz|123)/로 바꿀 수도 있습니다.
이 패턴을 사용하면 rgb() 패턴을 확장하여 rgba() 패턴을 작성할 수도 있었을 것입니다. rgb()와 rgba()는 알파 채널을 제외한 거의 대부분이 일치하기 때문입니다. 하지만, 아쉽게도 자바스크립트 정규식 엔진에서는 이 기능을 지원하지 않아서 사용하려고 하면 문법 오류가 발생합니다.
그래서 rgba()에 대한 패턴을 새로 작성하여 추가해주어야 합니다. 우선 rgb 패턴을 복사해와서 rgba라 이름만 바꾸었습니다.

/^rgba\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\s*0*\1){2}\)$/

알파 채널은 0부터 1까지의 실수 또는 0%부터 100%까지의 실수입니다. 원래는 여러 가지 고려를 해야하는 상황이지만 일치하지 않아도 되는 패턴이 느슨하여 간단하게 만들었습니다. 실무에 사용할 계획이라면 아마 보강이 필요할 것입니다.

[.\d]+%?

쉼표 규칙을 포함하고 닫는 괄호 앞에 공백이 있을 수 있는 패턴을 추가하면 다음과 같이 rgba 패턴을 작성할 수 있습니다.

/^rgba\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\s*0*\1){2},\s*[.\d]+%?\s*\)$/

일단 여기까지 작성한 패턴을 이어붙여보겠습니다. 여러 개의 정규식 패턴을 이어 붙이면 서브 패턴의 번호가 달라질 수 있으므로 이에 따라 백 레퍼런스의 참조 번호를 수정해주셔야 합니다.

/^#([\da-f]{1,2})\1\1$|^rgb\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\s*0*\2){2}\)$|^rgba\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\s*0*\6){2},\s*\d{0,3}(\.\d+)?%?\s*\)$/i

이 패턴을 반영해보면 hsl()과 hsla() 패턴을 제외한 모든 패턴에 기대했던대로 동작하는 것을 알 수 있습니다. 이제 hsl()과 hsla() 패턴을 작성해보겠습니다.

3. hsl() 패턴
H,S,L 값 중 회색조를 만들기 위해 신경써야 할 값은 S, L 입니다. S가 0%이거나 L이 0%또는 100%이면 나머지 값에 상관없이 회색조가 되기 때문입니다. H값의 범위는 0부터 360까지이지만, 문제에서는 이 값을 특별히 확인하고 있지 않아서 간단히 작성했습니다. 이제 중요한 S, L 값입니다. 이 두 값은 서로 관련성을 가지고 있어 한꺼번에 작성하겠습니다. S는 0%인지 아닌지가 중요한데 만약 0이라면 L을 대충 작성해도 된다는 뜻이 됩니다. 반대로 L이 0%또는 100%이면 S는 중요하지 않습니다. 실수 형태의 %값에 대한 패턴은 [\.\d]+%로 간단히 작성했습니다.

,\s*(0%,\s*[\.\d]+%|[\.\d]+%,\s*(0|100)%)

이제 이 값을 사용하며 hsl()에 대한 패턴을 만들면 다음과 같습니다.

/hsl\([.\d]+,\s*(0%,\s*[\.\d]+%|[\.\d]+%,\s*(0|100)%)\)$/

3-1. hsla() 패턴
rgb() → rgba()처럼 hsl() → hsla()도 알파 패턴을 추가하여 작성합니다.

/hsla\([.\d]+,\s*(0%,\s*[\.\d]+%|[\.\d]+%,\s*(0|100)%),\s*[\.\d]+\)$/

이제 각 패턴을 모두 이어붙이면 해답과 같은 정규식을 얻게 될 것입니다.

/^#([\da-f]{1,2})\1\1$|^rgb\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\s*0*\2){2}\)$|^rgba\((25[0-5]|(2[0-4]\d|\d{1,2}|1\d{2})(\.\d+)?%?)(,\s*0*\6){2},\s*\d{0,3}(\.\d+)?%?\s*\)$|^hsl\([.\d]+,\s*(0%,\s*[\.\d]+%|[\.\d]+%,\s*(0|100)%)\)$|^hsla\([.\d]+,\s*(0%,\s*[\.\d]+%|[\.\d]+%,\s*(0|100)%),\s*[\.\d]+\)$/i[/toggle]

댓글을 남겨주세요