Hey, Scripting Guy!온라인 토스터

Microsoft Scripting Guys

코드 다운로드 위치: HeyScriptingGuy2008_09.exe(150 KB)

현대 사회를 단 두 마디로 표현하자면 연결 사회라고 할 수 있습니다. 휴대폰 덕분에 집에서 전화가 오기를 기다릴 필요 없이 밤이든 낮이든 어디서나 자유롭게 다른 사람과 통화할 수 있습니다. (아! 이 얼마나 멋진 세상입니까.) 무선 컴퓨팅 덕분에 꼭 사무실이 아니더라도 집, 공원 벤치 등 어디서나 편리하게 일할 수 있습니다.

일화를 하나 소개하자면, 얼마 전 Scripting Editor의 부모님께서 캠핑 여행을 떠나셨는데 캠프장의 무선 네트워크에 연결하는 데 문제가 있어 마치 위대한 탐험가 루이스와 클락처럼 원시적인 생활을 할 수밖에 없었다고 합니다. 위성 TV가 잘 작동한 게 그나마 다행이었죠.

이건 시작에 불과했습니다. GPS 장치는 현재 위치를 수십 센티미터 단위로 정확하게 알려 줍니다. 장치에 따라서는 자신의 정확한 위치를 다른 사람에게 알릴 수도 있습니다. ("달아날 수는 있지만 숨을 곳은 없다"는 옛말이 딱이죠.) 이 칼럼을 쓰고 있는 Scripting Guy는 마음만 먹으면 당좌 계좌에서 발행한 수표가 결제될 때마다 휴대폰으로 메시지를 보내도록 할 수 있습니다. 자동차가 매달 상태를 보고하는 전자 메일을 보내도록 할 수도 있죠. 심지어는 필자가 휴가를 가면 토스터가 강아지를 산책시키고 화분에 물을 주기도 합니다.

흠, 맞습니다. 뭐 마지막 이야기는 아직 실현되지는 않았습니다. 하지만 필자가 마음만 먹으면 얼마든지 인터넷을 지원하는 토스터를 구입할 수 있습니다. 그리고 집에 가는 길에 전화로 토스터에 지시하여 도착할 쯤에 따끈한 토스트가 준비되도록 할 수 있습니다. 솔직히 따끈한 토스트를 미리 준비해야 할 필요가 있나 싶기는 하지만요. 그래도 뭐 원한다면…

물론 모두가 이렇게 항상 네트워크에 연결되기를 원할 때 Scripting Guy(시류에 한 번도 편승한 적이 없는)만 연결 끊기를 주창한다고 해서 놀랄 일은 아닙니다. 그렇다고 휴대폰이나 랩톱 컴퓨터를 내던져 버리라는 의미일까요? 아닙니다. 아무리 Scripting Guy라도 그 정도로 멍청하지는 않습니다. 그들이 말하는 바는 연결이 끊어진 레코드 집합을 스크립팅 무기로 갖추라는 것입니다. 그래도 휴대폰이나 랩톱 컴퓨터를 버리겠다면 뭐, 할 수 없죠.

참고 Harris Interactive에서 실시한 설문 조사에 따르면 미국인의 43%가 휴가 중에 업무 관련 전자 메일을 확인하거나 보내기 위해 랩톱 컴퓨터를 사용한 적이 있다고 합니다. 그리고 미국인의 50% 이상은 휴가 중에 휴대폰으로 전자 메일 및/또는 음성 메일을 확인하는 것으로 나타났습니다. 그런데 더 놀라운 사실은 아예 여기에 포함되지도 않은 40%의 미국인은 1년 내내 휴가를 한 번도 가지 않는다는 것입니다.

많은 사람들이 기꺼이 연결이 끊긴 레코드 집합을 스크립팅 무기의 하나로 추가하리라는 점은 두말할 필요도 없지만, 문제는 연결이 끊긴 레코드 집합이 무엇인지를 모른다는 거죠. 이 개념이 생소하게 느껴지는 독자를 위해 간단히 설명하면, 연결이 끊긴 레코드 집합은 실제 데이터베이스에 연결되지 않은 데이터베이스 테이블로서 스크립트에 의해 생성되고 메모리 내에만 존재하며 스크립트가 끝나는 순간 사라집니다. 바꿔 말해 연결이 끊긴 레코드 집합이란 단 몇 분 동안만 존재한 후 데이터와 함께 사라지는 인공 데이터 구조라고 할 수 있습니다. 정말 유용할 것 같지 않습니까? Scripting Guy에게 고맙다는 인사를 해야겠군요.

솔직히 연결이 끊긴 레코드 집합이 그다지 흥미롭게 들리지는 않는다고요? 맞습니다. 하지만 사용하기에 따라 매우 유용할 수 있습니다. 전문 VBScript 작성자라면 다 잘 알겠지만 VBScript의 데이터 정렬 기능이 최고 수준이라고 할 수는 없습니다. (세계의 어떤 데이터 정렬 기능도 최고라고 하기에는 부족하다고 생각하는 분들도 있겠지만) 마찬가지로, 대량의 데이터를 처리하는 VBScript 기능도 한정되어 있습니다. 속성이 2개 이하인 항목을 작업에 사용하도록 제한하는 Dictionary 개체나 대개 단일 속성 데이터 목록으로 제한되는 배열, 이 정도가 다입니다.

연결이 끊긴 레코드를 사용하면 다른 여러 문제와 함께 이 두 가지 문제를 해결할 수 있습니다. 데이터, 특히 다중 속성 데이터를 정렬해야 하는 경우? 문제 없습니다. 앞서 설명했듯이 연결이 끊긴 레코드 집합은 가상 데이터베이스 테이블과 같은데, 세상에서 데이터베이스 테이블을 정렬하는 것보다 쉬운 일은 없습니다. (예예, 알겠습니다. 엄밀하게 따지면 물론 데이터베이스 테이블을 아예 정렬하지 않는 편이 더 쉽죠.) 또는 속성이 여러 개 있는 많은 수의 항목을 추적하려는 경우? 역시 문제 없습니다. 다시 한번 말하지만 연결이 끊긴 레코드 집합이 가상 데이터베이스 테이블과 같으니까요. 정보를 일정 방식으로 필터링하건 데이터에서 특정 값을 검색해야 하는 경우? 가상 데이터베이스 테이블만 이용할 수 있다면 문제가 없는데 말이죠.

자, 이제 본론으로 들어가야 할 때인 것 같군요. (이쯤이면 본론에서 어떤 내용을 설명할지 다 이해했으리라 봅니다.) 우선 그림 1에서 MLB.com 웹 사이트에서 수집하여 탭으로 구분된 값 파일 C:\Scripts\Test.txt에 저장한 야구 통계를 살펴보겠습니다.

그림 1 탭으로 구분된 값 파일에 저장한 통계

선수 홈런 타점 타율
D Pedroia 4 28 .276
K Kouzmanoff 8 25 .269
J Francouer 7 35 .254
C Guzman 5 20 .299
F Sanchez 2 25 .238
I Suzuki 3 15 .287
J Hamilton 17 67 .329
I Kinsler 7 35 .309
M Ramirez 12 39 .295
A Gonzalez 17 55 .299

그런데 이 선수 목록을 홈런 수를 기준으로 정렬하여 표시해야 한다고 가정해봅니다. 이 경우 연결이 끊긴 레코드 집합이 도움이 될까요? 지금부터 알아보도록 하죠. 먼저 그림 2를 살펴보십시오. 그렇습니다. 코드가 복잡하죠? 보기만큼 어렵지는 않으니 너무 겁먹지는 마십시오.

그림 2 연결이 끊긴 레코드 집합

Const ForReading = 1
Const adVarChar = 200
Const MaxCharacters = 255
Const adDouble = 5

Set DataList = CreateObject("ADOR.Recordset")
DataList.Fields.Append "Player", _
  adVarChar, MaxCharacters
DataList.Fields.Append "HomeRuns", adDouble
DataList.Open

Set objFSO = _
  CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile _
  ("C:\Scripts\Test.txt", ForReading)

objFile.SkipLine

Do Until objFile.AtEndOfStream
    strStats = objFile.ReadLine
    arrStats = Split(strStats, vbTab)

    DataList.AddNew
    DataList("Player") = arrStats(0)
    DataList("HomeRuns") = arrStats(1)
    DataList.Update
Loop

objFile.Close

DataList.MoveFirst

Do Until DataList.EOF
    Wscript.Echo _
        DataList.Fields.Item("Player") & _
        vbTab & _
        DataList.Fields.Item("HomeRuns")
    DataList.MoveNext
Loop

먼저 상수를 4개 정의합니다.

  • ForReading: 이 상수는 텍스트 파일을 열고 데이터를 읽을 때 사용합니다.
  • adVarChar: Variant 데이터 형식을 사용하는 필드를 만드는 표준 ADO 상수입니다.
  • MaxCharacters: Variant 필드에 들어갈 수 있는 최대 문자 수(예: 255)를 나타내는 ADO 상수입니다.
  • adDouble: 마지막 ADO 상수로, 실수(Double) 데이터 형식을 사용하는 필드를 만드는 데 사용됩니다.

상수를 정의한 후에는 다음 코드 블록이 나옵니다.

Set DataList = CreateObject _
    ("ADOR.Recordset")
DataList.Fields.Append "Player", _
    adVarChar, MaxCharacters
DataList.Fields.Append "HomeRuns", _
    adDouble
DataList.Open

스크립트의 이 부분에서 연결이 끊긴 레코드 집합을 실제로 설정하고 구성하게 됩니다. 이 작업을 수행하기 위해서는 먼저 ADOR.Recordset 개체의 인스턴스를 만들어야 합니다. 잘 알겠지만 이 개체는 가상 데이터베이스 테이블(즉, 연결이 끊긴 레코드 집합)을 만듭니다.

그리고 다음 코드 줄과 Append 메서드를 사용하여 레코드 집합에 새 필드를 추가합니다.

DataList.Fields.Append "Player", adVarChar, MaxCharacters

보다시피 복잡할 것이 하나도 없습니다. 다음 매개 변수 3개를 사용하여 Append 메서드를 호출하기만 하면 되니까요.

  • 필드 이름(Players)
  • 필드의 데이터 형식(adVarChar)
  • 필드에 저장할 수 있는 최대 문자 수(MaxCharacters)

Players 필드를 추가한 후에는 실수(adDouble) 데이터 형식 필드인 HomeRuns를 추가합니다. 필드를 모두 추가했으면 Open 메서드를 호출하여 레코드 집합이 열려 있으며 사용할 준비가 되었음을 선언합니다.

다음으로 Scripting.FileSystemObject의 인스턴스를 만들고 C:\Scripts\Test.txt 파일을 엽니다. 스크립트의 이 부분은 연결이 끊긴 레코드 집합과는 실질적인 관련이 없으며 단지 텍스트 파일에서 데이터를 검색하는 데 사용됩니다. 텍스트 파일의 첫째 줄에는 헤더 정보가 들어 있습니다.

Player     Home Runs     RBI        Average

예제 레코드 집합에는 이 정보가 필요 없기 때문에 파일을 연 후에 가장 먼저 SkipLine 메서드를 호출하여 첫째 줄을 건너뜁니다.

objFile.SkipLine

이제 실제 데이터가 있는 첫째 줄로 이동했으므로 파일의 나머지 내용을 줄 단위로 읽을 수 있는 Do Until 루프를 설정합니다. 파일에서 한 줄씩 읽을 때마다 해당 값이 strLine라는 변수에 저장되고, Split 함수가 탭에 도달할 때마다 줄바꿈하여 데이터 줄을 값의 배열로 변환합니다.

arrStats = Split(strStats, vbTab)

요점만 간단하게 설명했지만 텍스트 파일에서 정보를 검색하는 방법을 모두 잘 아시리라 믿습니다. 간단히 요약해서 루프를 처음 실행하면 arrStats라는 배열에 그림 3의 항목이 포함됩니다.

그림 3 배열 내용

Item Number Item Name
0 D Pedroia
1 4
2 28
3 .276

이제부터 흥미로워집니다.

DataList.AddNew
DataList("Player") = arrStats(0)
DataList("HomeRuns") = arrStats(1)
DataList.Update

여기서는 선수 1(D Pedroia)에 대한 정보를 연결이 끊긴 데이터 집합에 추가합니다. 레코드 집합에 레코드를 추가하려면 먼저 AddNew 메서드를 호출합니다. 그러면 작업에 사용할 수 있는 빈 새 레코드가 만들어집니다. 다음 코드 두 줄에서는 두 레코드 집합 필드(Player 및 HomeRuns)에 값을 할당하고 Update 메서드를 호출하여 해당 레코드를 레코드 집합에 정식으로 씁니다. 그리고 다시 루프의 맨 위로 돌아와서 파일의 다음 줄(다음 선수)에 대해 프로세스를 반복합니다. 간단하지 않습니까? 코드가 많기는 하지만 모두 간단하고 이해하기 쉽습니다.

레코드 집합에 선수가 모두 추가된 후에는 어떻게 될까요? 텍스트 파일을 닫은 후 다음 코드 블록을 실행합니다.

DataList.MoveFirst

Do Until DataList.EOF
  Wscript.Echo _
    DataList.Fields.Item("Player") & _
    vbTab & _
    DataList.Fields.Item("HomeRuns")
  DataList.MoveNext
Loop

첫째 줄에서는 MoveFirst 메서드를 사용하여 커서를 레코드 집합의 시작 부분으로 이동합니다. 이렇게 하지 않으면 레코드 집합에 데이터 중 일부만 표시될 수 있습니다. 다음으로 더 이상 처리할 데이터가 없을 때까지, 즉 레코드 집합의 EOF(end-of-fle) 속성이 True일 때까지 계속 실행되는 Do Until 루프를 설정합니다.

이 루프에서는 Player 필드와 HomeRuns 필드의 값을 출력할 뿐입니다. 여기서는 특정 필드를 나타내는 데 DataList.Fields.Item("Player")과 같은 다소 특이한 구문이 사용되었습니다. 이제 Move­Next 메서드를 호출하여 레코드 집합의 다음 레코드로 이동하면 됩니다.

두말할 필요도 없을 정도로 쉽습니다. 루프 실행이 완료되면 다음과 같은 결과를 얻게 됩니다.

D Pedroia       4
K Kouzmanoff    8
J Francouer     7
C Guzman        5
F Sanchez       2
I Suzuki        3
J Hamilton      17
I Kinsler       7
M Ramirez       12
A Gonzalez      17

그런데 이 결과만 봐서는 전혀 유용할 것이 없어 보입니다. 선수 이름과 홈런 합계가 그대로 출력되었을 뿐이고 홈런 수를 기준으로 정렬되지는 않았습니다. 왜 연결이 끊긴 레코드 집합이 데이터를 자동으로 정렬해 주지 않은 것일까요?

사실 합당한 이유가 있습니다. 정렬 기준이 될 필드를 지정하지 않았기 때문입니다. 하지만 MoveFirst 메서드 호출 바로 앞에 정렬 정보가 추가되도록 스크립트를 수정하기만 하면 됩니다. 즉, 스크립트의 해당 부분이 다음과 같이 됩니다.

DataList.Sort = "HomeRuns"
DataList.MoveFirst

여기서도 특별한 비법은 필요 없고 HomeRuns 필드에 Sort 속성을 할당하기만 하면 됩니다. 스크립트 실행 후 출력되는 결과를 살펴보겠습니다.

F Sanchez       2
I Suzuki        3
D Pedroia       4
C Guzman        5
J Francouer     7
I Kinsler       7
K Kouzmanoff    8
M Ramirez       12
J Hamilton      17
A Gonzalez      17

이제야 제대로 된 것 같군요. 한 가지만 빼고 말이죠. 보통 홈런 기록은 홈런을 가장 많이 친 선수가 맨 위에 오는 내림차순으로 나열됩니다. 그렇다면 연결이 끊긴 레코드 집합을 내림차순으로 정렬할 방법은 없을까요?

물론 있습니다. 다음과 같이 유용한 DESC 매개 변수를 추가해야 합니다.

DataList.Sort = "HomeRuns DESC"

DESC 매개 변수는 어떤 역할을 할까요? 바로 결과를 다음과 같이 정렬하는 것이죠.

A Gonzalez      17
J Hamilton      17
M Ramirez       12
K Kouzmanoff    8
I Kinsler       7
J Francouer     7
C Guzman        5
D Pedroia       4
I Suzuki        3
F Sanchez       2

여러 속성을 기준으로 정렬하는 데도 아무 문제가 없습니다. 정렬 순서에 각 속성을 할당하기만 하면 됩니다. 예를 들어 먼저 홈런을 기준으로 정렬하고 다시 타율을 기준으로 정렬하려면 다음 명령을 사용하면 됩니다.

DataList.Sort = "HomeRuns DESC, RBI DESC"

직접 스크립트를 실행하여 결과를 확인해보십시오. 휴가지에서 전자 메일을 확인할 때만큼 신나지는 않겠지만 나름대로 재미가 있을 겁니다.

참고 레코드 집합에 추가되지 않은 필드를 기준으로 정렬할 수는 없습니다. 이것이 의미하는 바가 무엇일까요? Sort 문에 RBI와 같은 속성을 추가하기 전에 스크립트의 적절한 위치에 다음 코드 줄을 추가해야 함을 의미합니다.

DataList.Fields.Append "RBI", adDouble

DataList("RBI") = arrStats(2)

출력 결과를 확인하려면 Wscript.Echo 문도 다음과 같이 수정해야 합니다.

Wscript.Echo _
  DataList.Fields.Item("Player") & _
  vbTab & _
  DataList.Fields.Item("HomeRuns") & _
  vbTab & DataList.Fields.Item("RBI")

연결이 끊긴 레코드 집합으로 또 무엇을 할 수 있을까요? 한 가지 들자면 모든 선수에 대해 모든 정보를 검색하여 타율을 기준으로 데이터를 정렬할 수 있습니다. (그렇게 하려면 우선 RBI와 Batting­Average라는 필드를 만들도록 원래 스크립트를 수정해야 합니다.) 출력은 다음과 같습니다.

J Hamilton      17      67      0.329
I Kinsler       7       35      0.309
A Gonzalez      17      55      0.304
C Guzman        5       20      0.299
M Ramirez       12      39      0.295
I Suzuki        3       15      0.287
D Pedroia       4       28      0.276
K Kouzmanoff    8       25      0.269
J Francouer     7       35      0.254
F Sanchez       2       25      0.238

좋습니다. 그런데 타율이 .300 이상인 선수 목록을 표시하려면 어떻게 해야 할까요? 즉, 특정 조건에 맞는 선수만 표시되도록 데이터를 제한하려면 어떻게 해야 할까요? 한 가지 방법으로 레코드 집합에 Filter를 지정할 수 있습니다.

DataList.Filter = "BattingAverage >= .300"

일반적으로 레코드 집합 필터는 데이터베이스 쿼리와 같은 용도로 사용됩니다. 즉, 반환된 데이터를 레코드 집합의 일부 레코드로 제한하는 메커니즘을 제공합니다. 예제의 경우 Batting­Average 필드 값이 30 이상인 레코드만 남기고 나머지는 모두 제외시키는 Filter를 지정하면 됩니다. 그러면 바로 우리가 원하는 다음과 같은 결과를 얻게 됩니다.

J Hamilton      17      67      0.329
I Kinsler       7       35      0.309
A Gonzalez      17      55      0.304

우리 애들도 지금 여러분과 같은 반응을 보여 주면 좋을 텐데 말이죠.

그건 그렇고 단일 필터에 여러 조건을 사용할 수도 있습니다. 예를 들어 다음 명령은 반환되는 데이터를 BattingAverage 필드가 .300 이상이고 HomeRuns 필드가 11 이상인 레코드로 제한합니다.

DataList.Filter = _
  "BattingAverage >= .300 AND HomeRuns > 10"

반면 다음 필터는 BattingAverage 필드가 .300이거나 HomeRuns 필드가 11 이상인 레코드로 데이터를 제한합니다.

DataList.Filter = "BattingAverage >= .300 OR HomeRuns > 10"

두 스크립트를 모두 실행해보면 그 차이를 확인할 수 있을 겁니다. 흠, 내친 김에 다음 필터도 한번 실행해보십시오.

DataList.Filter = "Player LIKE 'I*'"

여기서 보듯이 필터에 와일드카드도 사용할 수 있습니다. 와일드카드를 사용하려면 등호 대신 LIKE 연산자를 사용하고 dir C:\Scripts\*.txt 같은 MS-DOS® 명령을 사용할 때처럼 별표를 사용합니다. 위의 예제 스크립트를 실행하면 이름이 문자 I로 시작하는 선수 목록이 출력됩니다. "그 다음 문자는 관계없이 Player 필드 값이 I로 시작하는 모든 레코드의 목록을 표시"하도록 지시하는 구문을 사용했기 때문입니다. 이제 무슨 말을 할지 이미 눈치챘으리라 믿습니다. 맞습니다. 직접 실행해보십시오.

그런데 타율이 0.309로 표시되는 데 대해서는 걱정할 필요가 없습니다. (일반적으로 타율은 앞에 0을 붙이지 않고 .309와 같이 표시합니다.) FormatNumber 함수를 사용하면 원하는 방식으로 타율의 형식을 지정할 수 있으니까요.

FormatNumber (DataList.Fields.Item("BattingAverage"), 3, 0)

숫자를 표시할 때 이 함수를 Wscript.Echo 문에 추가하기만 하면 됩니다. (또는 출력 결과에 변수를 할당하고 해당 변수를 Echo 문에 추가해도 됩니다.)

Wscript.Echo _
  DataList.Fields.Item("Player") & _
  vbTab & _
  DataList.Fields.Item("HomeRuns") & _
  vbTab & DataList.Fields.Item("RBI") & _
  vbTab & _
  FormatNumber _
  (DataList.Fields.Item("BattingAverage"), _
   3, 0)

재미 있으셨습니까?

아쉽게도 이번 달 칼럼은 여기서 마무리해야겠군요. 마지막으로 요약하자면... 아, 죄송합니다. 전화가 오는군요.

어쨌든 마지막으로 하고 싶은 말은... 이런, 이번에는 휴대폰이 울리는군요. 토스터가 이런 전자 메일도 보냈고요. "중요: 따끈한 토스트가 준비되었습니다. 버터와 잼 중 무엇을 바를까요?" 어쩔 수 없이 이만 끝내야 할 것 같습니다. 하지만 다음 달에 다시 만날 테니 너무 아쉬워하지는 마십시오.

Dr. Scripto의 Scripting Perplexer

매달 퍼즐 풀기 실력뿐만 아니라 스크립팅 능력까지 시험해 볼 수 있는 문제입니다.

2008년 9월: 스크립팅 단어 찾기

간단한(어려울 수도 있겠지만) 단어 찾기 퍼즐을 하나 소개하겠습니다. 목록에서 VBScript 함수와 명령문을 모두 찾으면 됩니다. 한 가지 알아야 할 점은 나머지 문자를 조합하면 Windows PowerShell™ cmdlet가 된다는 사실입니다.

단어 목록: Abs, Array, Atn, CCur, CLng, CInt, DateValue, Day, Dim, Else, Exp, Fix, InStr, IsEmpty, IsObject, Join, Len, Log, Loop, LTrim, Mid, Month, MsgBox, Now, Oct, Replace, Set, Sin, Space, Split, Sqr, StrComp, String, Timer, TimeValue, WeekdayName.

fig08.gif

정답:

Dr. Scripto의 Scripting Perplexer

정답: 2008년 9월: 스크립팅 단어 찾기

puzzle_answer.gif

Scripting Guys는 Microsoft에서 고용되어 일하고 있는 Microsoft의 직원들입니다. 이들은 좋아하는 야구 경기와 기타 여러 활동을 하는 시간을 제외하고는 항상 TechNet 스크립트 센터를 운영합니다. 자세한 내용은 www.scriptingguys.com에서 확인하십시오.