프날 오토핫키 강좌  v2

⚠ 이 강좌는 오토핫키 v2를 다룹니다

지금 보시는 강좌는 과거 오랜 시간동안 알려진 오토핫키(v1.1)의 차세대 버전인 오토핫키 v2를 다루고 있습니다.
만약 구버전인 '오토핫키 v1.1'의 강좌를 찾으신다면 프날 오토핫키 강좌(https://pnal.kr)를 봐주시면 되지만, 새로 오토핫키를 배우신다면 v2 버전을 배우시는 것을 강력히 추천드립니다.

66. 맵


배열의 짝꿍인 '맵'을 알아보겠습니다.

배열은 순서를 기반으로 한 자료 구조였습니다. 각 요소는 그 순서, 즉 '인덱스'에 종속되어 있었습니다. 어떤 요소를 사용하려면 첨자 연산자를 통해 해당 인덱스의 요소에 접근해야했죠. 다른 말로 하면, 배열에선 어떤 요소가 순서와는 관련없을 때에도 해당 요소를 사용하려면 그 요소의 인덱스를 알아야 했습니다.

예를 들어서, 어떤 화상의 x좌표와 y좌표를 저장하는 용도의 pos 배열이 있다면, pos[1]pos[2]처럼 x, y와 관련없는 1, 2라는 표지만으로 그들을 구분해야 했습니다. 앞선 객체에서의 예처럼 pos.x, pos.y와 같이 직관적인 이름을 사용할 수 없습니다. 이것이 가독성 측면에서의 배열의 단점입니다. 물론, 순번이나 개수를 표지로 삼을 때에는 배열이 적합하지만요.

은 값의 기준을 인덱스가 아닌 내가 만든 키(Key)로 삼는 자료구조입니다. 관련된 값을 묶어서 저장할 때, 단순한 숫자가 아니라 유의미한 단어와 함께 저장하는 것은 꽤 유용합니다. 아래 사진을 보세요!

Korean: 100, English: 90, Math: 80, Science: 80, Society: 90 사진 1. 각 과목에 대한 성적을 저장할 땐, 배열보단 맵이 적합합니다.

배열을 써서 위와 같은 성적을 관리한다면 각 과목이 몇번 인덱스에 저장되어 있는지 알아야합니다. 국어는 [1]로, 영어는 [2]로... 그러나 맵을 사용하면 국어는 Korean으로, 영어는 English로 관리할 수 있습니다. 읽기 쉬운 코드가 좋은 코드입니다.

Tip: 키와 값

맵은 키와 값 쌍으로 자료를 관리하는 자료구조입니다. 살짝 나온 용어인데, 키와 값에 대해서 좀 더 자세하게 말해보겠습니다.

(Key)는 자료를 구별할 수 있는 이름입니다. 배열에서 인덱스와 같이, 다른 값끼리는 겹칠 수 없는 고유한 이름이어야 합니다. 그리고, 대소문자를 구분합니다.

(Value)은 키에 대응되어 담기는 내용이며, 배열로 치면 요소에 해당합니다. 이곳엔 수나 문자열은 물론 객체, 다른 배열이나 맵 또한 올 수 있습니다. 보통은 수나 문자열만 넣습니다.

맵의 생성과 할당, 사용

맵을 만들어보고, 맵 안에 값을 넣어 사용해보겠습니다.

생성

맵의 생성은 배열의 경우와 비슷합니다.

1report := Map()
report 맵의 생성

배열과 마찬가지로, 초깃값을 넣어서 생성해줄 수도 있습니다. 이 때에는 키와 값을 콤마로 구분하여 순서대로 넣어줍니다.

1report := Map("Korean", 100, "English", 90, "Math", 80)
초깃값이 있는 report 맵의 생성

배열은 배열 리터럴 [] 안에 값을 나열에서 간단히 쓸 수 있었지만, 맵에는 '맵 리터럴'이란 것이 없습니다. 따라서, 새로운 맵 인스턴스를 만드는 Map() 구문을 사용해주어야만 합니다.

Korean, 100, English, 90, Math, 80 사진 2. 만들어진 맵의 모습

할당

첨자 연산자 안에 키를 적어주어서 원하는 키의 값에 접근 가능합니다. 배열과 유사하죠? 다만, 키를 문자열로 적어주었기 때문에 따옴표 표시를 해주어야 한다는 점은 주의해야 합니다.

1report["English"] := 95
맵에서 English 키의 값을 수정(재할당)

배열과 다르게, 없던 키에 할당해도 정상적으로 할당됩니다. 해당 키-값 쌍이 추가되는 것이죠.

1report["History"] := 100
맵에 History 키로 100 값을 할당 Korean, 100, English, 95, Math, 80, History, 100 사진 3. English 값이 수정되고 History 키-값 쌍이 추가된 맵의 모습

사용

사용 또한 배열과 같습니다. 인덱스 대신 키가 들어간다는 점만 주의해주세요.

1MsgBox(report["Korean"])
맵의 Korean 값을 출력

맵에서 사용할 수 있는 메서드와 속성

맵은 키와 값이 함께 저장되는 자료 구조이며, 배열과는 다른 여러 메서드를 지원합니다. 이곳에선 자주 사용되는 메서드만 적겠습니다.

메서드매개변수반환값역할
Delete()Key제거된 값해당 키-값 쌍 제거
Has()Keytrue 혹은 false해당 키의 존재 여부 반환

오토핫키에서 true는 1, false는 0과 완전히 동일하다는 점 다시 한 번 알려 드립니다.

맵에서 쓸 수 있는 속성도 있습니다. 이 역시 자주 사용되는 속성만 적어 보겠습니다.

속성의미
Count맵에 있는 키-값 쌍의 수
CaseSense맵의 키의 대소문자 구분 여부 (기본값은 On이며, Off로 설정시 구분 안함)

예를 들어서, map.CaseSense := "Off"처럼 작성하면 키의 대소문자 구분을 하지 않습니다. 이를 이용하여 별도로 설정하기 전까지 키는 대소문자를 구분합니다! 즉, CaseSense를 설정하지 않으면 대소문자가 다른 키는 서로 다른 값을 가집니다. 오토핫키에서 대소문자를 구분하는 몇 안되는 경우 중 하나입니다.

for-loop

배열은 순차적으로 증가하는 인덱스가 있었기 때문에, LoopA_Index를 이용하여 내부의 값을 모두 탐색할 수 있었습니다. 그러나 맵은 인덱스 대신 키를 기준으로 값을 저장하기 때문에 각 요소를 순차탐색 할 수 없습니다. 실제로 넣은 순서대로 값이 저장되지도 않고요.

대신, for-loop라는 또다른 방식의 반복문으로 맵 내부의 키-값 쌍을 모두 탐색할 수 있습니다. 기본 문법은 아래와 같습니다.

1for key, value in mapVar
2    MsgBox("키: " key " 값: " value)

for 뒤에 적는 두 개의 변수는 반복문 내에서 사용할 키와 값 변수입니다. 즉, 위 예시에선 반복문 내에서 키는 key, 값은 value 라는 변수에 담깁니다. in 예약어 뒤에 적은 변수는 탐색할 객체의 이름입니다. 여기선 mapVar 맵을 탐색하겠네요.

맵 내부의 모든 키-값 쌍을 이를 통해 순회할 수 있습니다. 아래에서 실습해보면서 감을 익혀보세요.

실습

1. 맵 탐색하며 평균 구해보기

위에서 만들었던 report 맵의 평균을 구해보겠습니다. 값의 평균은 모든 값을 더한 후 개수로 나누면 됩니다.

1sum := 0
2report := Map("Korean", 100, "English", 95, "Math", 80, "History", 100 )
3for subject, score in report
4    sum += score
5avarage := sum / report.Count
6MsgBox(avarage)
예제 1. report 맵의 평균 (모든 값의 합 / 값의 개수)

평균을 구하는 방법만 안다면 어렵지 않게 이해할 수 있는 코드입니다.

참고로, 반복문 안에서 subject 변수는 사용되지 않았는데, for-loop에서 쓰는 key, value 변수 중 필요하지 않은 변수는 생략할 수 있습니다. 저는 혼동을 줄이기 위해 (성능 상 문제가 없는 경우) 생략하지 않는 편입니다.

1for , score in report
만약 subject 변수를 생략하면 위와 같이 써질 것입니다.

2. 원하는 배열 구성해보기

프로그램을 작성할 땐 "원하는 동작을 어떻게 만들어나갈지 구상하고 실현하는 능력"이 제일 중요합니다. 이번엔 상황을 하나 가정해보고 그 상황에 맞는 맵을 작성해보겠습니다.

[만들고자 하는 프로그램]

개인용 학습 기록 프로그램
- 오늘 날짜, 내가 한 공부 주제, 시작 시간, 총 공부 시간을 기록하는 프로그램을 만든다.
- F1키는 공부 시작 버튼으로 이용한다.
- F2키는 공부 끝 버튼으로 이용한다. 공부가 끝남과 동시에 공부 기록을 보여준다.

위와 같은 프로그램을 만든다고 할 때, '오늘 날짜', '공부 주제', '시작 시간', '총 공부 시간'을 저장할 수 있는 맵을 만들면 좋겠네요. F1키는 누를 때마다 맵에 새 날짜와 시간을 담아 저장해보겠습니다. 그리고 F2키는 '공부 끝' 버튼으로서 누르면 '총 공부 시간'을 기록하게 해봅시다. '총 공부 시간'은 (현재 시간 - 공부 시작 시간)으로 하면 적절하겠군요. '공부 주제'는 '행정법 총론'으로 고정 하겠습니다.

1studyLog := Map()
2
3F1::
4{
5    studyLog["Date"] := A_Year "-" A_Mon "-" A_MDay
6    studyLog["Subject"] := "행정법 총론"
7    studyLog["StartTime"] := A_Now
8}
9
10F2::
11{
12    if (studyLog.Count = 0)
13    {
14        MsgBox("공부 시작 버튼을 먼저 눌러주세요.")
15        return
16    }
17    studyLog["TotalTime"] := A_Now - studyLog["StartTime"]
18    MsgBox(
19        "날짜: " studyLog["Date"]
20        "`n과목: " studyLog["Subject"]
21        "`n시작한 시간: " studyLog["StartTime"]
22        "`n공부한 시간: " studyLog["TotalTime"] "초"
23    )
24    ExitApp
25}

코드가 좀 길지만 천천히 읽어보세요. 우선 1번 줄에서 전역 변수인 studyLog 맵을 만듭니다. 그 뒤 F1이 눌러지면 Date, Subject, StartTime 키에 각각에 맞는 값을 넣습니다. 여기까진 이해가 되죠?

F2키를 누르면, 우선 studyLog 맵이 비어있는지 확인합니다(12번 줄). 맵에 있는 키-값 쌍이 0개라는 말은 맵이 비어있다는 뜻인데, 이는 곧 F1키를 누르지 않았다는 뜻입니다. 이런 경우 return을 통해 지역을 벗어나줍니다. 즉, 그 아래의 코드가 실행되지 않도록 해줍니다.

17번째 줄에서 총 공부 시간을 계산해줍니다. 단순히 A_Now에서 공부 시작 시간을 감해준 값입니다.

마지막으로 18번 줄부터 23번 줄까지는 MsgBox를 이용하여 값을 출력해주었습니다. 이 부분은 좀 눈여겨 볼만합니다. 네 줄에 걸쳐서 MsgBox의 Text 인수를 적어주었는데요, 중간에 콤마를 찍지 않았기 때문에 저게 전부 하나의 인수입니다. 한마디로 저 18 ~ 23번 줄은 아래 구문과 같습니다.

18MsgBox("날짜: " studyLog["Date"] "`n과목: " studyLog["Subject"] "`n시작한 시간: " studyLog["StartTime"] "`n공부한 시간: " studyLog["TotalTime"])
가로 스크롤바를 잡고 스크롤 해보세요.

한 줄이 너무 길기 때문에 여러 줄에 나누어서 적어준 것이지요. 따라서, 아래와 같은 형식을 외워두면 긴 줄을 하나의 인수로 적을 때 유용합니다.

1FunctionName(
2    "기이이이이"
3    "이이이이이"
4    "이이이이이"
5    "이이이이이"
6    "이이이이이"
7    "이이이이인 인수!"
8)
긴 인수를 여러 줄로 분할

직접 프로그래밍을 해보면서 맵을 만들고 조작해보세요. 이해가 안되어 복잡한 머릿속을 한결 환하게 밝혀줄 것입니다. 조금만 더 힘내보세요! 배열과 맵 파트만 끝나면 잠시 쉬었다가, 재미있는 GUI 프로그래밍 파트 진도를 나가면 됩니다. 기대되지 않나요?

질문하러 가기