개발자들이 꺼려하는 까칠한 규식이 형

정규식(Regular Expression)과 슬랙 채널 공유로 주문서 이름에 특수 문자 입력을 막은 에피소드

태초에 미션이 있었다

주문자 이름에 특수 문자가 입력이 되지 않게 하라!

위와 같은 특명이 티켓과 함께 나에게 주어졌다.
주문자 이름에 특수 문자가 들어가면 백엔드 시스템에서 오류가 나고 있었던 상황이었다.

특수 문자 필터링을 규식이 형에게 부탁해야지

까칠한 규식이 형

여기서 잠깐 개발자들이 좀처럼 꺼려하는 까칠한 우리 규식이 형을 잠시 소개하고 시작하는 게 좋겠다.

난 규식인데 블라블라...

이름 정규식 (正規式, Regular Expression)
특징 독특한 외계어를 많이 써서 의사소통에 어려움이 있지만,
조금만 친해지면 여러 방면에서 두루두루 도움을 주는 츤데레.

규식이 형 말투

규식이 형 말투는 주로 /정규 표현식/[플래그] 와 같이 이루어져 있다.
예) /\w\d/gi; 여기서 \w\d가 '정규표현식'이고 gi가 '플래그'다.

아래 기본 표현식과 플래그 정도만 알아도 웬만한 형의 말투는 대부분 이해할 수 있을 것이다.

플래그 설명
i (Case-insensitive) 대소문자 구분 없이 매치
g (Global) Greedy하게 가능한 모두 매치
m (Multi-line) 다중행 매치
표현식 설명
x|y x 또는 y 문자 중 하나와 일치
예) /green|red/는 "green apple"에서 "green"과 매치, "red apple"에서 "red"와 매치
[x] []안의 한 문자와 일치
예) [abc] a b c 중 한 문자와 일치
(x) ()안의 문자들을 메모리에 할당,
예) (bo)y boy중 bo는 $1로 치환할 수 있음
(?:x) ()안의 문자들을 메모리에 할당하지 않음,
예) (?:boy|girl) boy나 girl중 하나와 매치, $1로 치환 불가
^ 라인의 시작 지점과 매치
주의) [^0-9] []안의 ^는 Not을 의미함(예제는 숫자가 아닌 모든 문자)
$ 라인의 끝 지점과 매치
[a-z] 알파벳 소문자와 매치, [A-Z]대문자와 매치
\d (digit) 숫자와 매치, [0-9]와 동일
\w (word) 알파벳 대소문자, 숫자 및 _(언더바)와 매치, [A-Za-z0-9_] 와 동일
\s 스페이스, 탭, 줄바꿈 문자(White space character) 문자와 매치
예) ` /\s\w*/는 "foo bar"에서 " bar"와 매치`
\r 개행 문자(Carriage return)
\n 개행 문자(Line feed)
. 개행 문자(\n, \r, \u2028 or \u2029)를 제외한 임의의 모든 문자와 매치
x? x를 포함하거나 포함하지 않아도 매치(1번 또는 0번)
x+ 반드시 하나 이상 x를 포함하는 문자와 매치
예)"boo bii foo".replace(/(bo+)/g, "($1)");는 "(boo) bii foo"를 출력
x* x를 포함하거나 포함하지 않아도 모두 매치
예)"boo bii foo".replace(/(bo*)/g, "($1)");는 "(boo) (b)ii foo"를 출력

\대문자\소문자의 반대를 뜻한다.
예를 들어 \D[^\d], 즉 [^0-9]와 같은 의미이며 숫자를 제외한 모든 문자와 매치 된다.
\S[^\s]와 같으며 스페이스, 탭, 줄바꿈 문자를 제외한 모든 문자와 매치 된다.

몇 가지 실제 예제

각 예제 코드는 브라우저 콘솔 창에서 테스트 가능하다.

쉿! 이건 비밀인데… 엠덴 문서만 읽어도 규식이 형과 완전 절친이 될 수 있다는 사실!

달을 가리키는 손가락을 보지 말고 달 뒤에 숨겨진 영광을 보라!

다시 본론으로 돌아와서 입력 문자 중에서 특수 문자를 제거하기 위해서

스택 오버플로 바에서 신나게 춤을 추고 있던 규식이 형을 찾아서 아래와 같은 정규식을 얻어왔다.

// 규식이 형 曰 : 키보드에 있는 모든 특수 문자를 무시하게 만들어라!
txt.replace(/[\{\}\[\]\/?.,;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/g, '');

복잡해 보이지만 단순히 특수 문자들을 공백으로 치환하는 정규식이다.
생각대로 잘 동작하는 거 같아서 실 서버에 배포를 했었다.

어… 그런데 며칠이 지나자 다시 주문자 이름에 특수 문자들이 입력된다는 거였다.
음… 뭐가 잘못되었을까?
아! 내가 특수 문자 집단을 너무 작게 봤었음을 느꼈다.

가리키는 손가락을 보지 말고 저 멀리 있는 달을 보라

실수 영역에서 무리수가 유리수에 비해 엄청나게 많은 것 처럼 문자열에서 특수 문자들의 크기는 일반 문자에 비해 무한대에 가까웠다.

생각을 뒤집어서 '한글, 영문, 숫자, 빈칸'이 아닌 문자를 제외하는 방법으로 선회하는 게 더 좋을 거 같았다.

SDD(슬랙 Driven 개발 방법론)

다시 동네 친구 구그리(?)와 함께 규식이 형을 찾아가서 커피 한 잔 사주고 아래와 같은 정규식을 받았다.

// 규식이 형 曰 : '한글, 영문, 숫자, 빈칸'이 아닌 문자를 제외 시켜라!
txt.replace(/(?:[^\u3131-\u314e|^\u314f-\u3163|^\uac00-\ud7a3|^a-z|^0-9|^\s]|[\^\|])/ig, '');
/**
   * \uAC00-\uD7A3 : 가-힣 (음계)
   * \u3131-\u314E : ㄱ-ㅎ (자음)
   * \u314F-\u3163 : ㅏ-l (모음)
*/

\u3131유니코드 3131로 'ㄱ'문자에 해당한다.

맨 뒤 ^, | 문자가 앞단에서 걸러지지 않는 것이 찜찜해서 혹시나 하고 개발팀 슬랙 채널에 공유해서 더 좋은 방법이 있는지 문의했다.

SDD(Slack Driven Programming)

한 사람보단 두 사람이 낫다는 말처럼 불과 하루가 채 되지 않아 여러 명의 개발자들과 QA팀의 도움으로 아래와 같이 멋진 식이 완성되었다.

  /**
   * 입력 문자열에서 한글, 숫자, 영문을 제외한 모든 문자를 제거
   *
   * \uAC00-\uD7A3 : 가-힣 (음계)
   * \u3131-\u314E : ㄱ-ㅎ (자음)
   * \u314F-\u3163 : ㅏ-l (모음)
   * \u318D\u119E\u11A2\u2022\u2025a\u00B7\uFE55 : 천지인 키보드 문자
   * 참조 : http://bit.ly/39Ji0qA
   * '가-힣'으로 할경우 euc-kr 에서는 작동하지 않음.
   *
   * @param { string } txt 특수 문자가 포함된 텍스트
   * @return { string } 특수 문자가 제거된 텍스트
   */

export const filterSpecialChars = txt => {
  const regExp = /(?:[^\w\s\uAC00-\uD7A3\u3131-\u314E\u314F-\u3163\u318D\u119E\u11A2\u2022\u2025a\u00B7\uFE55]|_)+/g;

  if(regExp.test(txt)) {
    txt = txt.replace(regExp, '');
  }

  return txt;
};

혹시 혼자 풀기 어려운 난제에 괴로워하고 있는가?
지금 당장 개발팀 슬랙 채널에 올려서 같이 풀어 보는 것도 하나의 좋은 방법론이라 생각된다.
몹프로그래밍이란 방법도 있으니 시도해 보자! :)

글을 마무리하며

이 글을 통해 개발자들이 조금이라도 우리 규식이 형에게 친근감을 가질 수 있는 기회가 되었으면 좋겠다는 마음으로 적었습니다.
부족한 부분이 많습니다. 개선점이나 의견이 있으시면 거침없이 댓글 부탁드립니다.
앞으로도 개발 생활하면서 맞닥뜨린 에피소드들이 있으면 이곳에 좀 더 재미있게 공유하도록 노력하겠습니다.
즐개발하세요~!!!

이미지 출처