이 글은 프로그래밍 경험이 없는 분들을 대상으로 자바스크립트 웹 앱 제작법을 알려드리는 ‘웹 개발 배우기’ 시리즈의 일부인데요.
이번 시간에는 특정 입력값을 다른 출력값으로 ‘매핑(mapping)‘해주는 ‘맵(Map)‘이라는 자료 구조에 대해 알아볼 건데요.
이 Map을 활용해서 터미널에 텍스트를 거꾸로 출력하는 아주 재미있는 프로젝트도 함께 해볼 겁니다.
자바스크립트의 Map 클래스 #
‘맵(Map)‘은 ‘엔트리(entry)‘들의 모음인데요.
각 엔트리는 ‘키(key)‘와 ‘값(value)‘으로 이루어진 한 쌍입니다.
키를 이용해서 값을 찾아낸다는 점에서 마치 사전과 비슷하거든요.
예를 들어 영한사전처럼, 영어 단어를 주면 스페인어 단어를 찾아볼 수 있습니다.
그래서 이 자료 구조를 종종 ‘딕셔너리(dictionary)‘라고 부르기도 합니다.
Map 만들기 #
Map 클래스를 호출하여 Map을 만들었는데요.
그 인자는 키-값 쌍의 배열입니다.
각 쌍 또한 배열에 저장됩니다.
const englishToSpanish = new Map([
['yes', 'sí'],
['no', 'no'],
['maybe', 'quizás'],
]);
배열을 사용하는 것은 더 간결하다는 장점과 덜 설명적이라는 단점이 있습니다.
Map의 .size
#
Map은 자신의 엔트리 개수를 나타내는 .size 프로퍼티를 가지고 있거든요.
> englishToSpanish.size
3
배열은 그 요소들의 순서가 중요하기 때문에 .length를 가지고 있다는 점에 주목하세요.
반면에 Map에서는 엔트리의 순서가 중요하지 않습니다.
그래서 .size를 가지고 있는 겁니다.
값 가져오고 설정하기 #
Map 안에서는 하나의 키가 단 하나의 엔트리만 가질 수 있는데요.
.get() 메서드로 키에 해당하는 값을 찾아올 수 있고, .set() 메서드로 키에 연결된 값을 변경할 수 있습니다.
> englishToSpanish.get('maybe')
'quizás'
> englishToSpanish.set('maybe', 'tal vez');
> englishToSpanish.get('maybe')
'tal vez'
만약 존재하지 않는 키로 값을 찾으려고 하면 .get()은 undefined를 반환하는데요.
이때 .set()을 사용하면 새로운 엔트리를 추가할 수 있습니다.
> englishToSpanish.set('hello', 'hola');
> englishToSpanish.get('hello')
'hola'
키 존재 여부 확인하기 #
.has() 메서드는 특정 키가 Map에 존재하는지 확인할 수 있거든요.
> englishToSpanish.has('yes')
true
> englishToSpanish.has('goodbye')
false
키로서의 원시 값 vs. 객체 #
Map의 키로 객체를 사용할 수도 있는데요.
하지만 Map의 키 비교는 === 연산자와 비슷하게 동작하기 때문에, 새로운 빈 객체로는 위 Map의 값을 찾아올 수 없습니다.
따라서 객체는 드물게 좋은 Map 키가 되는데요 (아쉬운 점이죠).
하지만 동일한 객체를 가리키는 변수를 사용하면 값을 찾아올 수 있습니다.
import * as assert from 'node:assert/strict';
const key = {};
const objToStr = new Map([
[key, 'empty object'],
]);
assert.equal(
objToStr.get(key), 'empty object'
);
앞으로는 assert의 import 구문은 종종 생략할 테니 참고해주세요.
?? 연산자
#
자바스크립트는 결과값이 없을 때 종종 ‘값이 없음’을 의미하는 undefined를 사용하는데요.
이럴 때 ?? 연산자를 사용하면 undefined일 경우를 대비한 ‘기본값’을 아주 간단하게 지정할 수 있습니다.
> 'real value' ?? 'default'
'real value'
> undefined ?? 'default'
'default'
함수 안에서 활용하면 이런 모습이 됩니다.
> const useDefault = x => x ?? 'no value';
> useDefault(undefined)
'no value'
.get()은 키를 찾지 못하면 undefined를 반환하기 때문에 ?? 연산자와 함께 쓰면 아주 유용하거든요.
아래 코드는 사전에 단어가 없을 경우 (not found)를 반환합니다.
const lookUpWord = (word) => {
return englishToSpanish.get(word) ?? '(not found)'
};
assert.equal(lookUpWord('mountain'), '(not found)');
Map 활용 예제 문자 개수 세기 #
countChars() 함수는 문자열을 받아서 각 문자가 몇 번 등장했는지 세고, 그 결과를 Map으로 반환하는데요.
function countChars(chars) {
const charCounts = new Map();
for (let ch of chars) { // (A)
ch = ch.toLowerCase(); // (B)
const prevCount = charCounts.get(ch) ?? 0; // (C)
charCounts.set(ch, prevCount + 1);
}
return charCounts;
}
(A) for-of 반복문으로 문자열의 각 코드 포인트를 순회하고요.
(B) toLowerCase()를 사용해 ‘a’와 ‘A’를 같은 문자로 취급합니다.
(C) ?? 연산자를 사용해서 아직 등록되지 않은 문자의 이전 개수를 0으로 처리하는 것이 이 코드의 핵심입니다.
for 반복문
#
for-of 반복문과 이름은 비슷하지만, 실제로는 while 반복문과 더 가까운 for 반복문도 있는데요.
문법은 다음과 같습니다.
for (<선언>; <조건>; <액션>) {
// 본문
}
선언부는 반복문 시작 전에 한 번 실행되고, 조건부는 매 반복마다 체크되며, 액션은 매 반복이 끝난 후에 실행됩니다.
배열 메서드로 반복하기 .map()과 .filter()
#
배열에는 반복 작업을 아주 우아하게 처리해주는 여러 메서드들이 있는데요.
그중 .map()과 .filter()는 새로운 배열을 만들어 반환한다는 공통점이 있습니다.
array.map()
#
array.map()은 배열의 모든 요소를 하나씩 방문하면서, 인자로 받은 함수(콜백 함수)를 실행하고 그 결과를 모아 새로운 배열을 만드는데요.
> [1, 2, 3].map(num => num * num)
[ 1, 4, 9 ]
실무에서는 주로 객체 배열에서 특정 프로퍼티만 추출할 때 아주 유용하게 사용됩니다.
array.filter()
#
array.filter()는 이름 그대로 배열의 요소를 ‘필터링’하는 역할을 하는데요.
콜백 함수가 true를 반환하는 요소들만 모아서 새로운 배열을 만듭니다.
> [0, -1, 4, -8, 7].filter(x => x < 0)
[ -1, -8 ]
이름이 같아요 Map vs. .map() #
자료 구조 Map과 배열 메서드 .map()은 이름이 같아서 헷갈릴 수 있는데요.
둘 다 무언가를 다른 무언가로 ‘매핑’한다는 공통점이 있지만, 전혀 다른 개념이니 주의해야 합니다.
노드제이에스 셸 인자 다루기 #
전역 변수인 process 객체의 argv 프로퍼티는 현재 셸 명령어와 그 인자들을 배열 형태로 담고 있거든요.
process-argv.js 파일을 만들고 아래 코드를 작성한 뒤, 터미널에서 node process-argv.js first "two words"와 같이 실행해 보세요.
console.log(process.argv);
결과는 다음과 비슷할 겁니다.
[
'/usr/bin/node',
'/home/robin/learning-web-dev-code/projects/process-argv.js',
'first',
'two words'
]
인덱스 0에는 노드제이에스 실행 파일의 경로가, 인덱스 1에는 실행한 스크립트 파일의 경로가 들어있고요.
그 이후부터 우리가 전달한 인자들이 순서대로 들어있는 것을 확인할 수 있습니다.
프로젝트 upside-down-text.js
#
드디어 오늘의 최종 프로젝트입니다.
이 프로젝트는 입력받은 텍스트를 거꾸로 뒤집어진 유니코드 문자로 변환해서 출력하는데요.
node upside-down-text.js "How are you?"
출력 결과는 다음과 같습니다.
¿noʎ ǝɹɐ ʍoH
문자 변환을 위한 Map 설정하기 #
먼저 일반 문자를 뒤집어진 문자로 변환하기 위한 Map을 만들어야 하는데요.
const dict = new Map();
const addToDict = (source, target) => {
const srcArr = Array.from(source);
const trgArr = Array.from(target);
if (srcArr.length !== trgArr.length) { // (A)
throw new Error();
}
for (let i = 0; i < srcArr.length; i++) {
dict.set(srcArr[i], trgArr[i]);
}
};
addToDict(`ABCDEFGHIJKLMNOPQRSTUVWXYZ`, `ⱯꓭƆꓷƎℲ⅁HIꓩꓘ⅂ꟽNOԀꝹꓤS⊥ՈɅ𐤵X⅄Z`);
// ... 소문자, 숫자, 특수문자 추가 ...
(A)에서 우리는 안전 검사를 수행하고 실패하면 오류를 던집니다.
거꾸로 된 문자열 만들기 #
이제 dict를 사용해서 실제로 문자열을 뒤집는 함수를 만들어 보겠습니다.
const upsideDown = (str) => {
return Array.from(str)
.reverse()
.map(
(srcChar) => dict.get(srcChar) ?? srcChar
).join('');
};
코드는 다음과 같은 단계를 거칩니다.
-
문자열을 코드 유닛 배열로 변환합니다.
-
.reverse()메서드로 배열의 순서를 뒤집습니다. -
.map()메서드로 배열의 각 문자를dict를 이용해 뒤집어진 문자로 변환합니다. -
.join('')메서드로 배열을 다시 하나의 문자열로 합칩니다.
셸 입출력 처리하기 #
마지막으로, 셸로부터 입력을 받고 처리 결과를 출력하는 코드를 추가하면 프로젝트가 완성됩니다.
const arg = process.argv[2]; // (A)
console.log(upsideDown(arg)); // (B)