Skip to main content
  1. Posts 리스트/

웹 개발 배우기 11편 - 글자를 뒤집는 마법, 자바스크립트 Map 완전 정복

·952 words·5 mins·
웹 개발 배우기 - This article is part of a series.
Part : This Article
웹 개발 배우기 11편 - 글자를 뒤집는 마법, 자바스크립트 Map 완전 정복

이 글은 프로그래밍 경험이 없는 분들을 대상으로 자바스크립트 웹 앱 제작법을 알려드리는 ‘웹 개발 배우기’ 시리즈의 일부인데요.

이번 시간에는 특정 입력값을 다른 출력값으로 ‘매핑(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'
);

앞으로는 assertimport 구문은 종종 생략할 테니 참고해주세요.

?? 연산자
#

자바스크립트는 결과값이 없을 때 종종 ‘값이 없음’을 의미하는 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('');
};

코드는 다음과 같은 단계를 거칩니다.

  1. 문자열을 코드 유닛 배열로 변환합니다.

  2. .reverse() 메서드로 배열의 순서를 뒤집습니다.

  3. .map() 메서드로 배열의 각 문자를 dict를 이용해 뒤집어진 문자로 변환합니다.

  4. .join('') 메서드로 배열을 다시 하나의 문자열로 합칩니다.

셸 입출력 처리하기
#

마지막으로, 셸로부터 입력을 받고 처리 결과를 출력하는 코드를 추가하면 프로젝트가 완성됩니다.

const arg = process.argv[2]; // (A)
console.log(upsideDown(arg)); // (B)
웹 개발 배우기 - This article is part of a series.
Part : This Article