RegExp.Replace() 메서드의 활용

등록일시: 2002-05-26 22:49,  수정일시: 2018-04-07 23:12
조회수: 22,091
본문은 최초 작성 이후, 약 22년 이상 지난 문서입니다. 일부 내용은 최근의 현실과 맞지 않거나 동떨어져 있을 수 있으며 문서 내용에 오류가 존재할 수도 있습니다. 또한 본문을 작성하던 당시 필자의 의견과 현재의 의견에 많은 차이가 존재할 수도 있습니다. 이 점, 참고하시기 바랍니다.

본문에서는 RegExp 객체가 갖고 있는 세 가지 메서드들 중에서 Replace() 메서드에 관해서 중점적으로 살펴보도록 하자. 메서드 이름만 보고도 쉽게 예상할 수 있겠지만 RegExp.Replace() 메서드는 작업 대상 문자열에서 지정한 정규 표현식 패턴을 만족하는 문자열(들)을 찾고, 이를 지정한 문자열로 치환한다.

다음 코드는 ASP에서 Replace() 메서드를 사용하기 편리하도록 필자가 미리 만들어 놓은 함수로, 역시 필요한 분들은 각자의 상황에 알맞게 적절히 수정해서 사용하면 된다. 그리고, 이 코드는 이전글에서 Test() 메서드와 Execute() 메서드를 설명하기 위해서 만들었던 두 개의 함수, 즉 RegExpTest() 함수와 RegExpExec() 함수를 통해서 이미 익숙해진 패턴을 거의 동일하게 따르고 있으므로 별도의 추가적인 설명이 필요 없을 것으로 믿고 바로 글을 진행하도록 하겠다.

<%
  
  '******************************************************
  '*
  '* Public Function RegExpReplace(Patrn, TrgtStr, RplcStr)
  '*
  '*    RegExp.Replace() 메서드를 일반화한 함수
  '*
  '******************************************************
  
  
  Public Function RegExpReplace(Patrn, TrgtStr, RplcStr)
  
    Dim ObjRegExp
    
  On Error Resume Next
    
    Set ObjRegExp = New RegExp
    
    ObjRegExp.Pattern = Patrn               '** 정규 표현식 패턴
    ObjRegExp.Global = True                 '** 문자열 전체를 검색함
    ObjRegExp.IgnoreCase = True             '** 대.소문자 구분 안함
    
    RegExpReplace = ObjRegExp.Replace(TrgtStr, RplcStr)
    
    Set ObjRegExp = Nothing
    
  End Function
  
%>

그러면 이제 이 함수를 사용해서 실제로 실무에서 발생할 수 있을 법한 사례를 처리해보도록 하자. 다음은 Taeyo's ASP & ASP.NET의 포럼 중, ASP 게시판에서 발췌한 하나의 실제 사례이다. 이 질문을 올리셨던 분께는 필자가 E-Mail로 인용에 대한 양해를 드렸으며 그 분께서는 별다른 조건 없이 너무도 흔쾌히 인용을 허락을 해주셨다. 이 자리를 빌어서 다시 한 번 감사드린다.

이제 이 사례의 경우 발생한 문제점이 무었이었는지 다음의 글을 한 번 살펴보자.

데이타 베이스 내에서 가져올때 변환을 시켜서 가져올라고 하는데 좀처럼 쉽지 않네요.......
예를 들어 데이타 내에 테이블 사이즈인 width=500 이런것들이 불규칙하게 존재합니다......
그런데 그 데이타를 가져올때 테이블 크기땜에 자꾸 삐져나갑니다.....
그래서 Replace 함수를 써서 조금 변형 되게 가져 올라고 하는데.....특정한건 되지만
예를 들어 width 가 500 보다 큰것은 전부 500으로 바꾸어 줄라면 어떠케 해야 하나요??
데이타를 가져올때의 문제인가요??아님 방법이 없는걸까요??
도와주세요..........^^

즉 데이터베이스의 특정 컬럼에 HTML과 일반 문자열이 혼합된 일련의 문장들이 저장되어 있고, 이 문장들을 가져와서 미리 준비된 HTML 템플릿의 특정 위치에 출력하는 작업이 요구되는 상태다. 그러나, 한 가지 문제가 있는데, 가져와야 될 문자열 내부에는 HTML 태그들이 불규칙하게 섞여 있고, 이 중 일부 HTML 태그에 설정된 width 어트리뷰트(Attribute) 값 때문에 최종 출력물의 레이아웃이 깨지는 현상이 발생하는 것이다.

이런 상황은 게시판의 내용보기 페이지 등에서 HTML 태그의 출력을 허용하는 경우라든가, 또는 ASP.NET의 Code Behind 같이 유지, 보수를 용이하게 하기 위한 목적으로 ASP 코드와 HTML 문서를 분리하기 위해 자체적으로 HTML 템플릿을 정의해서 사용하는 경우 매우 흔하게 접할 수 있다. 이런 경우 대부분 문제가 되는 쪽은 기술적인 측면보다는, 데이터베이스에서 가져온 HTML 태그와 일반 문자열들의 혼합 문자열을 HTML 템플릿에 출력할 때 이 HTML 템플릿의 레이아웃이 파괴되어 버리는 현상이다.

가령, 프로그래머는 폭 500 픽셀의 테이블 안에 모든 내용을 출력하려는 생각으로 프로그램을 개발했다고 생각해보자. 그러나, 정작 사용자가 입력한 문자열 안에 폭 600 픽셀의 Table 태그가 포함되어 있다면, 결과적으로 폭 500 픽셀의 테이블 내부에 폭 600 픽셀인 테이블이 위치하게 된다. 따라서, 최종 출력물의 레이아웃이 일그러지는 것은 매우 당연한 일이고 위의 사례도 결국은 이 범주에 속한다고 말할 수 있을 것이다.

이런 동일한 사례를 설명하기 위해서 다음과 같은 간단한 약식의 HTML 문자열을 가정해보도록 하겠다. 지면 관계상 작업 대상 문자열을 매우 짧고 간단하게 구성했으나, 실제로는 보다 긴 문자열이나 HTML 태그 또는 일반 문자열들이 복잡하게 얽힌 문자열을 대상으로 하더라도 지금부터 설명하는 정규 표현식 처리의 결과는 동일하다.

  <table border=0 cellpadding=0 cellspacing=0 width=550>
  <table border=0 cellpadding=0 cellspacing=0 width=500>
  <table border=0 cellpadding=0 cellspacing=0 width=450>
  <table border=0 cellpadding=0 cellspacing=0 width=600>
  <table border=0 cellpadding=0 cellspacing=0 width="650">
  <table border=0 cellpadding=0 cellspacing=0 width='750'>

각각의 Table 태그 가장 끝 부분에 위치한 width 어트리뷰트를 주의해서 살펴보기 바란다. 여러 가지 경우를 가정하기 위해서 어트리뷰트의 값이 '"로 둘러쌓인 경우와 그렇지 않은 경우를 모두 포함시켰다. 일단은 가장 단순한 방법을 사용해서 이 문제를 해결해보기로 한다. 가령, RegExp.Replace() 메서드만을 사용해서 width 어트리뷰트의 값이 500보다 크면 그 값을 무조건 500으로 치환하는 경우를 가정해보겠다.

결론부터 말하면 위의 RegExpReplace() 함수를 이용해서 이 문제를 해결하기 위한 코드는 다음과 같다. 조금 복잡하게 보이겠지만 알고 나면 간단하므로 너무 걱정하지 말기 바란다. 그리고, 다시 한 번 강조하지만 이 코드에 사용된 정규 표현식 패턴은 단지 필자가 구성한 하나의 사례일 뿐, 이 문제에 대한 오직 하나뿐인 정답은 절대 아니며 이보다 더 효율적인 정규 표현식 패턴도 얼마든지 만들어 낼 수 있다는 점을 기억해두기 바란다.

RegExpReplace("(<table.*width=(?:|'|""))([56789]\d{2}|\d{4})((?:|'|"").*>)", Sample_String, "$1500$3")

이제 이런 결과가 나오기까지의 과정을 한 번 차분하게 살펴보도록 하자. 당면한 문제는 결국 세 자리 혹은 네 자리로 된 숫자들을 비교하고 500보다 큰 경우 500으로 치환해줘야 하는데, 거슬리는 부분은 숫자라고해서 아무 숫자나 막 바꿀 수가 없다는 것이다. 즉, HTML 태그가 아닌 일반 문자열 중에도 숫자값이 있을 수 있고 HTML 태그중에서도 특정 태그의 숫자만 바꿔야 하는 경우도 있다. 이에 더해서, 하나의 HTML 태그 내에서도 여러 개의 어트리뷰트들이 비슷한 숫자값을 가지는 경우가 허다하다.

따라서 우리가 원하는 HTML 태그에서 원하는 어트리뷰트의 값만 비교해서 치환을 해줘야 하는데 바로 이것이 난제인 것이다. 먼저, 우리의 관심의 대상인 width=xxx 부분을 살펴보도록 하자. 필자는 이 부분을 다음과 같이 구성했다.

width=(?:|'|")([56789]\d{2}|\d{4})(?:|'|")

당연히 이 정규 표현식 패턴의 가장 처음 부분은 width=로 시작된다. 그리고, 그 다음 부분에는 실제 숫자값이 위치하게 되는데 여기에서 한 가지 고려해야 할 사항이 있다. 실제로 현업에서 HTML 문서를 작성할 때는 width=xxx, width='xxx', 그리고 width="xxx"와 같은 세 가지 유형의 어트리뷰트 값 표현 방식들이 모두 비슷한 빈도로 사용된다는 점이다. 사견으로 이런 특성은 HTML의 가장 나쁜 일면 중 하나라고 생각하는데, 포멧팅을 강제하지 않는 것은 좋지만 결국 HTML 문서의 유효성(Validation)을 엉망으로 만드는 원인이 되기 때문이다.

따라서, 정규 표현식 패턴을 작성할 때도 이런 점을 고려해줘야 하고 필자는 이를 (?:|'|")라는 정규 표현식 패턴으로 구성했다. 즉 아무것도 없거나 '(작은따옴표)거나 "(큰따옴표)인 경우를 찾는 것이다. 이 때 한 가지 주의해야 할 점은, VBScript에서 "는 문자열을 의미하는 리터럴이므로, 실제로 RegExpReplace() 함수에서 사용할 때는 ""로 바꿔줘야 한다는 것이다.

그리고, 이 부분에서 단순히 소괄호를 사용하지 않고 ?: 메타 문자를 사용해서 (?:...) 형식으로 표현한 것은, 지난 글에서도 한 번 설명했지만 역참조를 사용하지 않기 위한 것이다. 일단 이 부분에 대해서는 잠시 설명을 미루고 넘어가도록 한다.

이번엔 실제로 치환이 이루어져야 할 숫자 부분을 살펴보자. 현재 조건을 고려해 볼 때 3자리 이하의 숫자는 치환할 필요가 없으며 5자리 이상의 숫자도 일반적으로 유통되는 그래픽 카드에서 지원되는 해상도를 고려해 볼 때 그다지 큰 의미가 없다. 따라서, 결국 3자리 숫자 또는 4자리 숫자만 고려하면 된다는 결론인데, 이런 조건은 ([56789]\d{2}|\d{4})라는 정규 표현식 패턴으로 표현할 수 있다. 즉, 5에서 9사이의 숫자로 시작되는 모든 3자리 숫자, 또는 모든 4자리 숫자를 찾는 것이다. 결국 이렇게 구성된 정규 표현식 패턴 조각들을 모두 조합하면 width=(?:|'|")([56789]\d{2}|\d{4})(?:|'|")라는 정규 표현식을 얻을 수 있다.

이제 그 다음 나머지 부분들에 관해 생각해 보자. 지금 필요한 작업은 문자열 내부에 존재하는 모든 width="xxx" 스타일의 문자열을 치환하는 것이 아니다. Table 태그 내부에 존재하는 width="xxx" 스타일의 문자열만을 치환해야 하는 것이다. 따라서, 정규 표현식 <table.*.*>를 위에서 만든 정규 표현식의 앞뒤에 추가하면 다음과 같은 결과가 나오게 된다.

<table.*width=(?:|'|")([56789]\d{2}|\d{4})(?:|'|").*>

지금까지의 과정을 모두 이해했다면 이제 정규 표현식 전체을 통틀어서 가장 흥미로운 기능이라고 말할 수 있는 역참조 기능에 대해서 알아볼 순서다. 위와 같이 구성된 정규 표현식 패턴 그 자제만으로는 당연히 치환이 불가능하다. 치환할 대상 문자열을 찾기 위해서 이처럼 정규 표현식 패턴을 구성했지만, 찾아낸 대상 문자열을 어떻게 치환할 것인지는 결정하지 않았기 때문이다. 정규 표현식에서는 패턴과 일치하는 문자열들을 찾아 놓고서 찾은 문자열의 일부분들을 조합하여 치환에 재사용할 수가 있는데, 이런 기능을 역참조라고 한다. 만약, 정규 표현식에서 이 역참조가 불가능했다면 문자열 처리 작업에 정규 표현식을 사용해야 할 이유가 크게 줄어들었을 것이다.

일반적으로 대상 문자열에서 치환을 하고자 하는 부분은 패턴 전체가 아니라 패턴의 일부분인 경우가 많고 지금의 사례와 같은 경우도 마찮가지다. 우리가 치환하기를 원하는 부분은 <Table ... > 태그 전체가 아니라, 단지, 그 중 일부인 width="xxx" 뿐이다. 이런 경우 역참조 기능이 매우 유용하게 사용되는데 다음을 보도록 하자. 위에서 작성한 정규 표현식을 크게 세 부분으로 나누었다.

<table.*width=(?:|'|")  +  ([56789]\d{2}|\d{4})  +  (?:|'|").*>

이 세 부분 중에서 우리가 치환하기를 원하는 부분은 단지 가운데 부분뿐이다. 오히려 처음 부분과 끝 부분은 바뀌면 절대 안된다. 이 정규 표현식을 다음과 같이 각각 소괄호로 묶어보자.

(<table.*width=(?:|'|"))  +  (([56789]\d{2}|\d{4}))  +  ((?:|'|").*>)

그리고, 다시 합친다.

(<table.*width=(?:|'|"))(([56789]\d{2}|\d{4}))((?:|'|").*>)

이제 정규 표현식이 완성되었다. 이렇게 소괄호를 잘 사용하면 재미있는 작업이 가능해진다. 이 정규 표현식 패턴으로 RegExp.Replace() 메서드를 실행시키면 패턴에 매치되는 각각의 문자열을 찾을 때마다 미리 약속된 변수명 $n으로 (여기서 n은 1, 2, 3...) 패턴의 소괄호 안에 있는 문자열들을 순서대로 저장해둔다.

가령, 본문의 예제에서 가장 첫 번째로 패턴과 일치하는 문자열은 <table border=0 cellpadding=0 cellspacing=0 width=550>이 될 것이다. 이 때 패턴에 사용된 소괄호를 따져보면 다음과 결론을 얻을 수 있다.

  • $1 이라는 변수에 문자열 "<table border=0 cellpadding=0 cellspacing=0 width="이 저장된다.
  • $2 이라는 변수에 문자열 "550"이 저장된다.
  • $3 이라는 변수에 문자열 ">"이 저장된다.

그리고, 더욱 고무적인 사실은 이 $n 변수를 바로 다음 코드와 같이 치환할 문자열에서 사용할 수 있다는 점이다. 아래에서 붉은색으로 강조된 부분이 바로 치환할 문자열을 나타내는 부분인데 결국 이 문자열은 역참조 변수 $1과 바꾸려는 값 500, 그리고 또 하나의 역참조 변수 $3이 결합된 문자열인 것이다.

RegExpReplace("(<table.*width=(?:|'|""))([56789]\d{2}|\d{4})((?:|'|"").*>)", Sample_String, "$1500$3")

따라서, 방금 예로 들었던 가장 첫 번째로 패턴과 일치하는 문자열, <table border=0 cellpadding=0 cellspacing=0 width=550>의 경우 다음과 같은 작업이 벌어지게 되고 <table border=0 cellpadding=0 cellspacing=0 width=550> 문자열은 그 작업 결과로 만들어진 <table border=0 cellpadding=0 cellspacing=0 width=500>으로 치환된다.

$1 + 500 + $3 = "<table border=0 cellpadding=0 cellspacing=0 width=" + "500" + ">"
              = "<table border=0 cellpadding=0 cellspacing=0 width=500>"

주의해야 할 점은 위에서도 한 번 얘기했던 것처럼 (?:...)과 같이 ?: 등의 메타 문자가 포함된 소괄호의 경우에는 따로 그 부분을 $n 변수에 저장하지 않는다는 것이다. 일련의 ?: 메타 문자 시리즈들은 바로 그런 목적을 위해 만들어진 것으로 그 외에도 각각 특수한 기능들을 가지고 있다. 이에 관한 추가적인 사항은 각자 알아보기 바란다.

다음은 지금까지 설명한 내용을 처리하는 실제 코드다. 출력을 위해서 Replace() 함수를 몇 번 호출하는 부분이나 검색 대상 문자열을 만드는 부분을 빼고 나면 지금까지의 긴 설명이 무색할 정도로 간단하다.

<%

  Dim Sample_String         '** 치환 대상 문장을 담을 변수
  Dim Result_String         '** 치환 결과를 담을 변수  
  Dim RegExp_String         '** 정규 표현식 패턴을 담을 변수  
  
  
  '** 검색 대상 문장  
  Sample_String = "<table border=0 cellpadding=0 cellspacing=0 width=550>" & vbCRLF & _
                  "<table border=0 cellpadding=0 cellspacing=0 width=500>" & vbCRLF & _
                  "<table border=0 cellpadding=0 cellspacing=0 width=450>" & vbCRLF & _
                  "<table border=0 cellpadding=0 cellspacing=0 width=600>" & vbCRLF & _
                  "<table border=0 cellpadding=0 cellspacing=0 width=""650"">" & vbCRLF & _
                  "<table border=0 cellpadding=0 cellspacing=0 width='700'>"
                  
  '** 정규 표현식 문장  
  RegExp_String = "(<table.*width=(?:|'|""))([56789]\d{2}|\d{4})((?:|'|"").*>)"
  
  '** RegExpReplace() 함수 실행
  Result_String = RegExpReplace(RegExp_String, Sample_String, "$1500$3")
  Result_String = Server.HTMLEncode(Result_String)
  Response.Write Replace(Result_String, vbCRLF, "<br>")
  
%>

정규 표현식에 관한 첫 번째 글에서 날짜 문자열을 처리하는 방법에 관해서 얘기했던 적이 있다. 이번에 그와 같은 날짜 문자열들에 대한 간단한 예를 살펴본다. '2000년 10월 13일'을 '10/13/2000' 형식으로 치환하려면 다음과 같은 코드를 사용하면 된다.

RegExpReplace("(\d{4})년 (\d+)월 (\d+)일", "2000년 10월 13일", "$2/$3/$1")

또한, 그 반대의 경우는 다음과 같다.

RegExpReplace("(\d+)\/(\d+)\/(\d{4})", "10/13/2000", "$3년 $1월 $2일")

이 정도 수준만 되도 불편했던 프로그래밍이 한결 수월해진다. 그러나, 당연한 얘기겠지만 RegExp.Replace() 메서드도 만능은 아니다. 예를 들어, 위의 사례에서와 같은 경우 Width 어트리뷰트의 값을 항상 500으로 치환해야 하는 것이 아니라, 기존의 값에 20을 더한 값으로 치환해야 한다면 위의 정규 표현식은 사용할 수 없다. 즉, 다음과 같은 코드는 원하는 대로의 결과가 나오지 않는다는 뜻이다.

RegExpReplace("(<table.*width=(?:|'|""))([56789]\d{2}|\d{4})((?:|'|"").*>)", Sample_String, "$1($2+20)$3")

이는 정규 표현식이 문자열들의 모든 요소를 말 그대로 문자열로 취급하기 때문인데, 이럴 때는 지난번 글에서 설명했던 RegExp.Execute() 메서드와 Matches Collection, SubMatches Collection 등의 기능과 함께 VBScript의 일반적인 함수들을 사용해서 추가적인 작업을 구성해줘야 한다. 사실, 역참조 변수 $n은 SubMatches Collection의 Item들과 정확하게 일 대 일로 대응되는 개념이다.

그리고, 필자의 사견으로는 VBScript의 정규 표현식 처리는 Perl과 같은 여타 다른 프로그래밍 언어들의 그것에 비교해 상당히 부족하다는 느낌이다. 물론, 구버전의 VBScript에서 정규 표현식이 지원조차 되지 않았던 것을 생각한다면 지금의 상태도 매우 큰 발전이겠지만 아쉬움이 남는 것은 어쩔수 없다. 그러나, 그렇다고 해서 VBScript의 정규 표현식에 투자하는 시간이 낭비라고는 생각하지 않는다. VBScript의 정규 표현식은 나름대로 Visual Basic.NET의 정규 표현식 기능으로 접근해 나가는 과도기적인 역활을 훌륭히 수행할 수 있다고 생각하며 그 자체만으로도 매우 강력한 기능을 갖고 있다.

또한, 정규 표현식 자체가 특정 프로그래밍 언어에 종속되는 것이 아니고 해당 언어에서 정규 표현식을 얼마나 충실하게 구현했는가에 따라 그 특성이 바뀌는 것이므로, 아마도 비교적 접근하기 쉬운 VBScript의 정규 표현식이 다른 프로그래밍 언어들의 정규 표현식 구현에 접근하기 위한 좋은 시작점이 될 수 있지 않을까 한다.

이번글 역시 분량이 필자의 예상을 휠씬 초과했다. 지난번 글에서도 얘기했지만 보내 이번글에서는 위에서 설명한 RegExp.Replace() 메서드와 함께 JavaScript의 정규 표현식을 간략하게나마 소개하고 넘어가려고 했었다. 그러나, 본문의 분량이 약간 어정쩡해져 버려서 애초에 의도했던 것과는 달리 이번 글은 이정도로 마무리해야 할 것 같다. JavaScript의 정규 표현식에 관한 내용도 나름대로 그 분량이 매우 많기 때문에 괜한 무리를 하는 것은 바람직하지 않다.

그리고, 다소 아쉽지만 다음글에서도 역시 JavaScript의 정규 표현식에 관한 내용은 다루지 못할것 같다. 지금 진행되고 있는 일련의 글들은 모두 VBScript 5.6의 특성에 관한 것으로 일종의 연재물적인 성격을 가지고 있다. 따라서, 지금 갑자기 JavaScript에 관한 이야기를 새롭게 시작하는 것은 전체적인 글의 흐름에 무리를 가져올 것 같다는 생각이다.

그런 이유로 JavaScript의 정규 표현식에 관한 얘기는 VBScript 5.6에 관한 글들이 모두 마무리된 후에야 비로소 다시 시작할 수 있을 것 같다. 다음글에서는 VBScript 5.6의 클래스(Class)에 관해서 살펴보고 지면이 허락된다면 이 클래스를 이용하는 Remote Scripting에 관해서도 같이 알아보도록 하겠다.