Skip to content
This page has been auto-translated and may contain errors.View in English

배열 메서드

docs.scrimba.com

가격 목록이 있고, 각 가격에 세금을 더한 두 번째 목록을 만들고 싶다고 해봅시다. 배열을 순회하면서 합을 계산하고 결과를 새 배열에 추가하는 for 루프를 작성할 수도 있습니다. 이렇게 해도 되지만, "각 항목을 가져와서 변형한다"는 작은 아이디어치고는 준비할 게 너무 많습니다. 배열 메서드는 바로 이런 종류의 작업을 위해 내장된 도구입니다. 목록에 무엇을 하고 싶은지만 말하면 되고, 그것을 실행하는 루프를 일일이 풀어 쓰지 않아도 됩니다. 이 장에서는 매일같이 손이 가게 될 배열 메서드들을 다룹니다. 이 메서드들은 배열함수를 그대로 활용하므로, 두 내용을 가까이에 두고 보시길 권합니다.

map: 모든 항목을 변형하기

map은 **메서드**입니다. 배열에 속해 있으면서 그 배열에 어떤 작업을 하는 함수입니다. map은 여러분이 준 작은 함수에 각 항목을 통과시켜 그 결과들을 모아 새 배열을 만듭니다. 그 함수를 넘겨주면, map이 항목을 하나씩 그 함수에 건네줍니다.

가격 목록이 있고 각 값을 두 배로 만들고 싶다고 해봅시다.

js
const prices = [2, 4, 6];
const doubled = prices.map((price) => price * 2);
console.log(doubled); // [4, 8, 12]

(price) => price * 2 부분은 화살표 함수입니다. 항목 하나(price)를 받아서 그것이 무엇으로 바뀌어야 하는지(price * 2)를 반환하는 짧은 함수입니다. map은 이 함수를 항목마다 한 번씩 호출해서 그 결과를 같은 순서로 새 배열에 담습니다.

map은 목록을 같은 길이의 새 목록으로 변형하는 배열 **메서드**입니다. map에게 콜백, 즉 항목마다 한 번씩 실행할 함수를 넘기면, 그 콜백이 반환한 값이 결과 배열의 같은 위치에 있는 항목이 됩니다.

js
const prices = [2, 4, 6];
const doubled = prices.map((price) => price * 2);
console.log(doubled); // [4, 8, 12]

이걸 수동으로 구현한 버전과 비교해 보면, 같은 일을 하면서도 신경 쓸 부분이 더 많습니다.

js
const prices = [2, 4, 6];
const doubled = [];
for (let i = 0; i < prices.length; i += 1) {
  doubled.push(prices[i] * 2);
}
console.log(doubled); // [4, 8, 12]

루프를 쓰면 인덱스와 새 배열, push를 직접 관리해야 합니다. map은 "각 가격을 가격의 두 배로 바꿔라"라는 의도를 그대로 표현하고, 세부 처리는 알아서 해줍니다.

map은 각 요소에 콜백(호출할 함수로 넘겨준 것)을 적용해 같은 길이의 새 배열로 변형합니다. 인덱스 i에서의 콜백 반환값이 결과의 인덱스 i가 됩니다. 아무것도 빠지지 않고 추가되지도 않으므로, 형태는 그대로 유지되고 값만 바뀝니다.

js
const prices = [2, 4, 6];
const withTax = prices.map((price) => price * 1.2);
console.log(withTax); // [2.4, 4.8, 7.2]

콜백은 두 번째, 세 번째 인자로 인덱스와 전체 배열도 받을 수 있어서, 위치가 중요할 때는 array.map((value, index) => ...) 형태를 쓸 수 있습니다. map이 주는 가치는 의도의 명확함입니다. map은 "같은 길이, 각 항목을 변형한다"는 것을 한눈에 보여줍니다. 반면 루프에서는 읽는 사람이 인덱스, 카운터, push로부터 그 의도를 다시 조립해야 합니다.

Junomapmap은 목록을 받아서 같은 길이의 새 목록을 돌려주는데, 그 목록의 모든 항목은 여러분이 만든 작은 함수를 통과한 결과입니다. 화살표 함수가 각 항목이 무엇으로 바뀌는지를 말해주고, map은 그것을 모든 항목에 순서대로 적용합니다. 루프를 써서 두 번째 목록을 만들고 있다는 걸 깨달을 때마다 map을 떠올리세요.
Junomapmap은 "모든 항목을 같은 길이의 새 배열로 변형한다"는 뜻입니다. 콜백이 항목마다 한 번씩 실행되고, 그 반환값이 같은 위치에 자리 잡습니다. 이것을 "루프 돌리고 push하는 패턴"의 대체물로 보게 되면, 루프의 절반은 읽기 쉬운 한 줄로 바뀝니다.
Junomapmap은 길이와 위치를 그대로 유지합니다. 인덱스 i에서의 콜백 반환값이 결과의 인덱스 i가 됩니다. 위치가 중요할 때를 위해 콜백은 인덱스와 배열도 받을 수 있습니다. 루프라면 카운터와 push로부터 의도를 재조립해야 하지만, map은 그 의도를 그대로 드러냅니다.

filter: 통과한 항목만 남기기

map이 모든 항목을 변형한다면, filter는 그중 일부만 남깁니다. 각 항목에 대해 예/아니오로 답하는 함수를 주면, filter는 "예"를 받은 항목들로만 새 배열을 만듭니다.

js
const scores = [40, 75, 90, 55, 88];
const passing = scores.filter((score) => score >= 60);
console.log(passing); // [75, 90, 88]

(score) => score >= 60 함수는 각 점수에 대해 true 또는 false를 반환합니다. filtertrue인 항목만 남기고 나머지는 버립니다. 새 배열은 원본보다 짧아질 수 있지만, 순서는 절대 바뀌지 않습니다.

filter는 콜백이 true를 반환하는 항목만 담긴 새 배열을 돌려줍니다. 이 콜백은 **프레디케이트**입니다. 각 항목에 대해 예/아니오 질문에 답하는 것이 이 함수의 역할입니다.

js
const scores = [40, 75, 90, 55, 88];
const passing = scores.filter((score) => score >= 60);
console.log(passing); // [75, 90, 88]

결과는 대개 입력보다 짧아지고, 원래 순서는 유지됩니다. 안에 if가 들어간 루프와 비교하면, filter는 테스트를 가장 앞세우고 그 주변의 형식적인 코드는 없앱니다. 콜백이 모든 항목에 대해 true를 반환하면 원본을 그대로 복사한 배열을 얻고, 모두 false를 반환하면 빈 배열을 얻습니다.

filter프레디케이트(참 또는 거짓 값을 반환하는 역할을 하는 콜백)가 참으로 판단하는 요소들로 이루어진 새 배열을 돌려줍니다. 길이는 유지되지 않습니다. 결과는 빈 배열부터 전체 복사본까지 다양하게 나올 수 있으며, 순서는 유지됩니다.

js
const scores = [40, 75, 90, 55, 88];
const passing = scores.filter((score) => score >= 60);
console.log(passing); // [75, 90, 88]

기억해 둘 만한 것이 두 가지 있습니다. 첫째, filter는 항목이 살아남을지 결정만 할 뿐 항목을 바꾸지는 않습니다. 그러니 선택과 변형을 동시에 해야 할 때는 map과 함께 쓰세요. 둘째, 콜백의 반환값은 불리언으로 강제 변환되므로, 프레디케이트가 실수로 0이나 undefined를 반환하면 남기려던 항목이 빠져버립니다. 명시적인 비교문을 반환하면 이런 종류의 버그는 사라집니다.

Junofilterfilter는 테스트를 통과한 항목만 남기고 나머지는 버리므로 새 배열은 대개 더 짧아집니다. 여러분의 함수가 각 항목에 대해 예/아니오로 답하고, "예"라고 답한 것만 통과합니다. 순서를 절대 바꾸지 않으므로, 남은 항목들은 원래 순서를 그대로 유지합니다.
Junofilterfilter는 콜백이 true를 반환하는 항목을 남겨서, 원본과 같은 길이거나 더 짧은 새 배열을 돌려줍니다. 콜백은 프레디케이트, 즉 변형이 아니라 예/아니오 테스트입니다. 모두 true면 전체 복사본, 모두 false면 빈 배열이 되고, 순서는 항상 유지됩니다.
Junofilterfilter는 선택하고 map은 변형합니다. 이 역할 구분이 명확하기 때문에 두 메서드가 잘 이어집니다. 콜백의 반환값은 불리언으로 강제 변환되므로, 프레디케이트가 슬쩍 0이나 undefined를 반환하면 원하던 항목이 조용히 빠져버립니다. 명시적인 비교문을 반환하면 이런 종류의 버그 전체가 사라집니다.

reduce: 값 하나로 압축하기

mapfilter는 둘 다 배열을 돌려줍니다. reduce는 배열을 값 하나로 압축하는 메서드입니다. 합계, 최댓값, 이어붙인 문자열 같은 것들이죠. 다른 메서드보다 헷갈리기 쉬우니 천천히 살펴볼 만합니다.

reduce는 배열을 순회하면서 진행 중인 결과 값을 함께 가지고 다닙니다. 각 항목마다 "지금까지의 결과와 이 항목을 가지고, 새로운 결과는 무엇인가?"를 묻습니다.

js
const prices = [10, 20, 30];
const total = prices.reduce((sum, price) => sum + price, 0);
console.log(total); // 60

이 함수는 두 가지를 받습니다. 지금까지의 합계인 sum과 현재 항목인 price입니다. 이 함수는 sum + price를 반환하고, 이 값이 다음 항목을 위한 새로운 합계가 됩니다. 마지막에 있는 0은 합계가 시작되는 값입니다. 그래서 0 + 10, 그다음 10 + 20, 그다음 30 + 30을 거쳐 60에 이릅니다.

reduce는 **누산기**를 가지고 다니면서 배열 전체를 하나의 값으로 만듭니다. 누산기는 각 단계가 갱신하고 다음 단계로 넘겨주는 진행 중인 결과 값입니다. 콜백은 누산기와 현재 항목을 받아서, 다음 단계를 위한 누산기를 반환합니다.

js
const prices = [10, 20, 30];
const total = prices.reduce((sum, price) => sum + price, 0);
console.log(total); // 60

명확히 구분해야 할 두 인자가 있습니다. sum은 누산기(지금까지 쌓인 값)이고, price는 현재 항목입니다. reduce의 두 번째 인자인 0은 누산기의 시작 값입니다. 단계별로 보면: 0에서 시작해서 10을 더해 10이 되고, 20을 더해 30이 되고, 30을 더해 60이 됩니다. 여기서 sum이라는 이름은 그냥 붙인 이름일 뿐입니다. 누산기는 어떤 타입이든 될 수 있고, 계속 이어붙이는 문자열이나 진행 중인 최댓값이 될 수도 있습니다.

reduce누산기(진행 중인 결과 값)를 배열 전체에 통과시켜 값 하나로 접습니다. 콜백은 (accumulator, currentItem) => nextAccumulator 형태이고, 그 반환값이 다음 단계에 넘겨질 누산기가 됩니다. reduce의 두 번째 인자는 초기 누산기입니다.

js
const prices = [10, 20, 30];
const total = prices.reduce((sum, price) => sum + price, 0);
console.log(total); // 60

reduce에서 발생하는 버그를 대부분 막아주는 실용적인 규칙이 두 가지 있습니다. 항상 초기값을 넘기세요. 초기값이 없으면 reduce는 첫 번째 요소를 시작값으로 쓰고 두 번째 요소부터 시작하는데, 이는 빈 배열에서는 예외를 던지고 누산기 타입이 요소 타입과 다를 때는 조용히 동작이 달라집니다. 그리고 콜백 안의 모든 경로는 누산기를 반환해야 합니다. 여러 줄로 된 본문에서 return을 빠뜨리면 다음 단계의 누산기가 undefined가 되어버립니다. reduce는 이 메서드들 중 가장 범용적입니다. mapfilter 둘 다 reduce로 다시 쓸 수 있는데, 바로 그 점 때문에 가장 읽기 어려워지기도 쉽습니다. 값 하나로 압축할 때 사용하고, 그렇지 않을 때는 더 단순한 방법을 쓰세요.

Junoreducereduce는 진행 중인 결과 값을 목록 전체에 걸쳐 가지고 다니면서 각 항목마다 갱신합니다. 콜백은 지금까지의 결과와 현재 항목을 받아서 새로운 결과를 반환합니다. 마지막 인자는 시작 값이고 보통 합계라면 0입니다. 저는 이걸 한 단계씩 소리 내어 읽어봤을 때 딱 이해가 됐습니다.
Junoreducereduce는 누산기를 이용해 배열을 값 하나로 압축합니다. 누산기는 각 단계가 갱신하고 넘겨주는 진행 중인 결과 값입니다. 콜백은 (accumulator, item) 형태이고 마지막 인자가 누산기의 시작값이 됩니다. 꼭 숫자여야 하는 건 아니고, 누산기는 계속 쌓아가는 문자열이나 진행 중인 최댓값이 될 수도 있습니다.
Junoreduce 항상 초기값을 넘기세요. 그렇지 않으면 reduce는 첫 번째 요소로 시작하고 빈 배열에서는 예외를 던집니다. 콜백 안의 모든 경로는 누산기를 반환해야 합니다. 그렇지 않으면 다음 단계는 undefined를 받게 됩니다. 이 메서드들 중 가장 범용적이면서 가장 읽기 어려워지기도 쉬우니, 값 하나로 압축할 때 쓰고 그렇지 않을 때는 더 단순한 방법을 쓰세요.

find, some, every

이 세 메서드는 filter와 가까운 친척입니다. 배열에 테스트를 적용한다는 점은 같지만, 새 배열이 아니라 더 좁은 질문에 답합니다.

find는 테스트를 통과한 첫 항목 하나를 돌려줍니다. 통과한 항목 전체 목록이 아닙니다. someevery는 배열 전체에 대한 참/거짓 질문에 답합니다.

js
const scores = [40, 75, 90, 55];

const firstPass = scores.find((score) => score >= 60);
console.log(firstPass); // 75

const anyPass = scores.some((score) => score >= 60);
console.log(anyPass); // true

const allPass = scores.every((score) => score >= 60);
console.log(allPass); // false

find는 처음으로 조건에 맞는 항목(75)을 반환합니다. some은 "통과한 항목이 하나라도 있는가?"를 물어서, 적어도 하나가 통과했으므로 true를 반환합니다. every는 "전부 통과했는가?"를 물어서, 4055가 통과하지 못했으므로 false를 반환합니다.

filter와 같은 프레디케이트 방식이지만, 답이 세 가지로 다릅니다. find는 **처음으로 조건에 맞는 항목**을 반환합니다(맞는 항목이 없으면 undefined). some은 항목이 하나라도 통과하면 true를 반환합니다. every는 전부 통과해야만 true를 반환합니다.

js
const scores = [40, 75, 90, 55];

console.log(scores.find((score) => score >= 60)); // 75
console.log(scores.some((score) => score >= 60)); // true
console.log(scores.every((score) => score >= 60)); // false

필터된 목록이 아니라 항목 하나를 원할 때는 find를, 컬렉션에 대한 예/아니오 답만 필요할 때는 some이나 every를 쓰세요. some으로 충분한 곳에 filter(...).length > 0을 써도 동작은 하지만, 모든 항목을 검사하고 버릴 배열까지 만들어서 some이 직접 답해주는 질문에 답하는 셈입니다.

새 배열을 만드는 대신 조건이 판단되는 즉시 멈추는 세 가지 프레디케이트 기반 쿼리입니다. find처음으로 조건에 맞는 요소 또는 undefined를 반환합니다. some은 처음으로 통과하는 요소가 나오면 true를 반환합니다. every는 처음으로 실패하는 요소가 나오면 false를 반환합니다.

js
const scores = [40, 75, 90, 55];

console.log(scores.find((score) => score >= 60)); // 75
console.log(scores.some((score) => score >= 60)); // true
console.log(scores.every((score) => score >= 60)); // false

여기서 핵심은 조기 종료입니다. someevery는 답이 결정되는 순간 멈추므로, 불리언 값만 필요할 때는 배열 전체를 도는 filter보다 비용이 적습니다. 알아둘 만한 두 가지 경계 사례가 있습니다. findundefined를 반환하는 경우와 실제로 undefined인 값을 찾은 경우를 구별할 수 없으므로, 가능하다면 반환값보다는 원하는 프레디케이트 자체를 테스트하세요. 그리고 빈 배열에서는 some이 항상 false이고 every는 항상 true입니다(실패할 항목이 없으므로 공허하게 참이 됩니다). 이는 맞는 동작이지만 처음 접하면 다들 한 번씩 놀랍니다.

Junofind, some, and everyfind는 통과한 첫 항목 하나를 주지, 전체 목록을 주지 않습니다. some은 "하나라도 통과했나?"를 묻고 every는 "전부 통과했나?"를 묻는데, 둘 다 truefalse로 답합니다. 필터된 배열이 아니라 항목 하나나 예/아니오 답을 원할 때 이것들을 쓰세요.
Junofind, some, and everyfilter와 같은 프레디케이트를 쓰지만 답은 세 가지로 좁혀집니다. find는 첫 매칭 항목 또는 undefined를 주고, some은 하나라도 통과하면 참, every는 전부 통과해야만 참입니다. 항목 하나나 불리언을 원할 때는 filter(...).length 대신 이것들을 쓰세요. 의도를 그대로 드러내고 버릴 배열을 만들지 않기 때문입니다.
Junofind, some, and everysomeevery는 답이 결정되는 즉시 멈추는 조기 종료 방식이라서, 불리언 값을 위해 배열 전체를 필터링하는 것보다 낫습니다. 두 가지 경계 사례를 조심하세요. findundefined를 반환하는 것은 실제로 undefined를 찾은 것과 똑같이 보이고, 빈 배열에서는 some이 항상 거짓이고 every는 항상 참입니다. 둘 다 맞는 동작이지만, 한 번씩은 다들 당황합니다.

체이닝과 가독성 유지하기

앞서 다룬 메서드들은 조합했을 때 이득이 되는 설계를 공유합니다. 각 메서드는 새로운 값을 반환하고 원본 배열은 그대로 둡니다.

map, filter, 그리고 나머지 메서드들은 처음에 가지고 있던 배열을 절대 바꾸지 않습니다. 새 배열(또는 값)을 돌려주고 원본은 그대로 둡니다.

js
const prices = [10, 20, 30];
const doubled = prices.map((price) => price * 2);
console.log(doubled); // [20, 40, 60]
console.log(prices); // [10, 20, 30]

mapfilter 둘 다 배열을 돌려주기 때문에, 이 둘을 나란히 이어 쓸 수 있습니다. 먼저 필터링하고, 살아남은 항목들을 map으로 처리하는 식입니다.

js
const prices = [10, 20, 30, 40];
const bigDoubled = prices.filter((price) => price >= 20).map((price) => price * 2);
console.log(bigDoubled); // [40, 60, 80]

이 메서드들은 원본을 바꾸지 않고 새 배열과 값을 돌려줍니다. 이런 속성을 **불변성**이라고 합니다. 원본 데이터는 그대로 유지됩니다. 이것이 체이닝을 안전하게 만들어주는 이유입니다. filter가 배열을 반환하므로, 그 결과에 바로 map을 호출할 수 있습니다.

js
const prices = [10, 20, 30, 40];
const bigDoubled = prices.filter((price) => price >= 20).map((price) => price * 2);
console.log(bigDoubled); // [40, 60, 80]

체인을 위에서 아래로 하나의 파이프라인처럼 읽으세요. "20 이상인 가격을 남기고, 남은 것들을 각각 두 배로 만든다"는 식입니다. 순서가 중요합니다. 먼저 filter를 하면 map이 더 적은 항목만 처리하게 됩니다. 체이닝은 두세 단계까지는 잘 읽히지만, 그 이상이 되면 이름을 붙인 중간 변수나 평범한 루프가 하나의 긴 체인보다 더 명확할 때가 많습니다.

이 메서드들은 원본을 변경하지 않는(non-mutating) 특성을 가집니다. 새 배열과 값을 반환하고 원본은 그대로 둡니다. 이 불변성 덕분에 filter(...).map(...) 체인을 파이프라인처럼 안심하고 읽을 수 있습니다. 어떤 단계도 뒤로 돌아가서 공유 배열을 몰래 편집하지 않으니까요.

js
const prices = [10, 20, 30, 40];
const bigDoubled = prices.filter((price) => price >= 20).map((price) => price * 2);
console.log(bigDoubled); // [40, 60, 80]

체인이 도움이 될지 방해가 될지는 두 가지 판단에 달려 있습니다. 첫째는 비용입니다. 각 단계는 배열을 순회하고 새 배열을 하나 할당하므로, 세 단계짜리 체인은 세 번 순회하고 중간 배열을 두 개 만듭니다. 일상적으로 다루는 크기에서는 이것이 문제되지 않고 가독성이 더 큰 이득이지만, 큰 배열을 다루는 성능이 중요한 경로에서는 문제가 될 수 있고, 이럴 때는 한 번의 순회로 처리하는 단일 루프나 reduce가 답입니다. 둘째는 명확성입니다. 각 단계가 하나의 명확한 아이디어일 때 체인은 제 값을 합니다. 콜백에 분기가 늘어나거나 체인이 세 단계를 넘어가는 순간, 이름을 잘 붙인 변수를 쓰는 평범한 for 루프가 대체로 더 잘 읽힙니다. 한 줄에 들어가는 버전이 아니라, 동료가 가장 빨리 이해할 버전을 선택하세요.

JunoChaining and staying readable 이 메서드들은 시작할 때 가진 배열을 절대 바꾸지 않고 새 배열을 주므로, 원본은 항상 안전합니다. filtermap 둘 다 배열을 반환하기 때문에 체인으로 이어 쓸 수 있습니다. 먼저 필터링하고, 남은 것들을 map으로 처리하세요. 체인은 위에서 아래로 한 단계씩 읽으세요.
JunoChaining and staying readable 원본 배열을 그대로 두는 불변성 덕분에 체이닝이 안전합니다. filter(...).map(...)을 위에서 아래로 파이프라인처럼 읽으세요. 순서가 중요하다는 것도 기억하세요. 먼저 필터링하면 map이 처리할 항목이 줄어듭니다. 두세 단계를 넘어가면, 이름을 붙인 변수나 평범한 루프가 하나의 긴 체인보다 대체로 더 명확합니다.
JunoChaining and staying readable 각 단계는 순회 한 번과 할당 한 번이므로, 평소 크기에서는 체인이 문제없고, 큰 데이터를 다루는 성능이 중요한 경로에서는 하나의 루프나 reduce로 합치는 것이 낫습니다. 명확성이 또 다른 판단 기준입니다. 각 단계가 하나의 명확한 아이디어일 때 체인은 제 값을 하고, 콜백에 분기가 늘어나면 평범한 루프가 이깁니다. 가장 짧은 버전이 아니라, 동료가 가장 빨리 읽을 버전을 쓰세요.

다음으로 가볼 곳

map, filter, reduce는 목록을 가지고 할 일 대부분을 다룹니다. 변형하거나, 좁히거나, 압축하는 것이죠. find, some, every는 더 좁은 질문에 답합니다. 이것들이 익숙해지면, 배열을 순회하던 대부분의 for 루프가 의도를 그대로 말해주는 한두 줄로 바뀝니다.

자연스러운 다음 단계는 객체입니다. 여기서는 배열이 단순한 숫자와 문자열보다 더 풍부한 항목들을 담게 되는데, 같은 메서드들이 그 항목들에도 그대로 동작합니다. 여기 나온 콜백 문법이 조금 불안하게 느껴졌다면, 함수를 다시 훑어보면 나머지가 훨씬 잘 이해될 것입니다.