JavaScript 기본

Array 함수 #1. 반복문

밀하우스 마나스톰 2022. 1. 7. 18:08

1. forEach와 map

 

let input = [0,1,2,3,4];
input.forEach(() => { console.log("forEach. 5번 출력됨.") });
input.map(() => { console.log("map. 5번 출력됨.") });

forEach와 map은 매개변수로 콜백 함수 하나를 받는다.

 

그리고 배열을 순회하면서 배열의 원소들을 탐색할 때마다 (원소를 마주할 때마다) 해당 함수를 실행한다.

 

 

let input = ['a','b','c','d'];

input.forEach((x, y, z) => {
    console.log("value : " + x);
    console.log("index : " + y);
    console.log("array : " + z);
});

input.map((x, y, z) => {
    console.log("value : " + x);
    console.log("index : " + y);
    console.log("array : " + z);
});

forEach와 map이 실행할 콜백 함수는 세 개의 인자를 가질 수 있다.

 

첫 번째는 탐색한 배열 원소의 값, 두 번째는 탐색한 배열 원소의 인덱스, 세 번째는 탐색하고 있는 배열 자체를 의미한다.

 

 

그래서 로그를 확인하면 위와 같이 출력된 것을 확인할 수 있다.

 

여기까지는 forEach와 map이 동일한 기능을 하고 있음을 알 수 있는데, 그렇다면 차이점은 뭘까?

 

 

let input = [0,1,2,3,4];
let output1 = input.forEach(x => { return x + 3 });     // undefined
let output2 = input.map(x => { return x + 3 } );        // [3,4,5,6,7]

map과 forEach와 다른 점. map은 탐색한 배열 원소를 저장할 수 있다는 것이다.

 

map은 콜백 함수의 return 결과값들로 새로운 배열을 구성한 뒤 최종적으로 리턴한다.

 

 

forEach와 map 함수의 원형을 보면 바로 알 수 있다.

 

 

어떤 배열을 가공해서 새로운 배열을 리턴 받고자 한다면 map을 쓰고,

 

새로운 배열을 리턴받을 필요 없이, 어떤 작업을 실행하는데 배열을 참조만 하는 상황이면 forEach를 쓰면 될 것 같다.

 

 

 

2. for vs forEach와 map

 

forEach와 map을 쓰는 가장 큰 이유는 for에 비해서 코드량을 줄일 수 있고 가독성을 향상시킬 수 있기 때문일 것이다.

 

그리고 for문을 사용하면 순회를 시작할 인덱스, 순회 종료 조건을 정의해야 하는데 이 부분에서 실수가 나올 수 있다.

 

(예를 들어, i < 10과 i <= 10 에서 실수할 가능성)

 

다만 forEach와 map은 배열의 모든 원소를 순회할 때까지 종료되지 않으며

 

for문에서는 사용할 수 있었던 break와 continue를 사용할 수 없다는 제한이 있다.

 

 

 

3. forEach에서 어떻게 하면 순회를 중단할 수 있을까?

 

(1) forEach에서 return 쓰기

let input = [0,1,2,3,4];
let numbers = [];
input.forEach(x => {
    if (x < 2) numbers.push(x);
    else return;
});
console.log(numbers);

위의 코드를 실행하면 로그는 [0,1]이 찍히는데 예상한 대로 찍혔다고 볼 수 있다.

 

그렇다면 return으로 break를 완벽히 대체할 수 있을까? 정답은 아니다.

 

 

let input = [0,1,2,3,4];
let numbers = [];
input.forEach(x => {
    if (x == 2) return;
    numbers.push(x);
});
console.log(numbers);

위의 코드는 [0,1]이 아닌 [0,1,3,4] 로그를 출력한다.

 

그렇다. forEach에서의 return은 break가 아닌 continue와 같은 기능을 한다고 볼 수 있다.

 

 

(2) try/catch 추가 정의하기

 

let input = [0,1,2,3,4];
let loopBreak = new Error('Break');

try {
    input.forEach(x => {
        if (x == 2) throw loopBreak;
    });
}
catch(e) {
    if (e != loopBreak) throw loopBreak;
}

이 방법은 정말 억지로 break를 대체하기 위해 try/catch를 쓰는 모양이고 코드 가독성에서도 좋아 보이지 않는다.

 

 

 

4. some

 

forEach에서 순회를 종료하는 방법에 대해 구글링을 하다 보면 some으로 대체할 수 있다는 얘기들도 있다.

 

Array 메서드 #2에서 설명할 함수라 여기서는 짧게 정리하고 가면,

 

some은 배열을 순회하면서 특정 조건을 만족하는 원소가 존재하는지 확인할 수 있는 함수다.

 

 

let input = [0,1,2,3,4];
if (input.some(x => { if(x % 2 == 0) return true })){
    console.log("짝수 있음")
}

보통은 이런 식으로 사용할 수 있는데, some 함수는 첫 번째 return true에서 배열의 순회가 종료되는 특징이 있다.

 

 

let input = [0,1,2,3,4];
let numbers = [];
input.some(x => {
    if (x == 2) return true;
    numbers.push(x);
});
console.log(numbers);

forEach에서 return을 사용하는 예시 코드에서 forEach를 some으로, return을 return true로 바꿨다.

 

이 코드의 결과는 성공적으로 [0,1]이 출력된다. return true가 break의 역할을 했다고 볼 수 있다.

 

하지만, 그렇다고 해서 forEach 대신 some을 쓰는 게 맞을까?

 

some이라는 함수 자체는 배열에 특정 조건을 만족하는 원소가 있는지 확인하는 함수로서의 인식이 일반적이다.

 

위의 코드는 numbers에 어떤 원소들이 담기는지 확인하는 것에 좀 더 포커싱이 맞춰져 있는 것 같다.

 

어떤 함수를 일반적이지 않은 의도로 사용하는 것도 코드 가독성을 해치는 큰 요인이다.

 

 

 

5. for-of

 

let input = [0,1,2,3,4];
let numbers = [];
for (const x of input){
    if (x == 2) break;
    numbers.push(x);
}
console.log(numbers);

for-of 는 ES6에 추가된 비교적 최신 문법이다. (for가 ES1, forEach가 ES5에 추가되었다)

 

생김새는 forEach와 비슷하면서, 그 안에서 for문처럼 break와 continue를 자유롭게 사용할 수 있다.

 

forEach는 세 개의 인자(원소의 값, 원소의 인덱스, 배열 전체)를 이용해 콜백 함수를 정의할 수 있었는데, for-of 에서도 가능하다.

 

 

let input = ['a','b','c','d'];
for (const x of input) console.log(x);                                  // a,b,c,d
for (const x of input.keys()) console.log(x);                           // 0,1,2,3
for (const [key, value] of input.entries()) console.log(key, value);    // 0 a, 1 b, 2 c, 3 d

for-of 문법은 위 처럼 사용할 수 있다.

 

단점이라면 타입스크립트 타겟 버전이 ES6 이전이라면 위 코드를 사용할 수 없다.

tsconfig.json 파일을 열어서 compilerOptions에 "downlevelIteration": true를 추가해줘야한다.

알다시피 타입스크립트에서 작성한 코드는 컴파일시 자바스크립트로 변환이 되는데,

 

forEach, map, some 등의 함수는 거의 원문 그대로 변환이 되는 반면에

위의 for-of 코드는 굉장히 많은 양의 코드로 변환이 된다.

 

 

var input = ['a', 'b', 'c', 'd'];
try {
    for (var input_1 = __values(input), input_1_1 = input_1.next(); !input_1_1.done; input_1_1 = input_1.next()) {
        var x = input_1_1.value;
        console.log(x);
    } // a,b,c,d
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
    try {
        if (input_1_1 && !input_1_1.done && (_a = input_1.return)) _a.call(input_1);
    }
    finally { if (e_1) throw e_1.error; }
}
try {
    for (var _d = __values(input.keys()), _e = _d.next(); !_e.done; _e = _d.next()) {
        var x = _e.value;
        console.log(x);
    } // 0,1,2,3
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
    try {
        if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
    }
    finally { if (e_2) throw e_2.error; }
}
try {
    for (var _f = __values(input.entries()), _g = _f.next(); !_g.done; _g = _f.next()) {
        var _h = __read(_g.value, 2), key = _h[0], value = _h[1];
        console.log(key, value);
    } // 0 a, 1 b, 2 c, 3 d
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
    try {
        if (_g && !_g.done && (_c = _f.return)) _c.call(_f);
    }
    finally { if (e_3) throw e_3.error; }
}

실제로 확인해보면 타입스크립트에서 작성한 위의 4줄의 코드가 자바스크립트로 위와 같이 변환된다.

 

(끔찍)

 

 

 

결론은, forEach와 map이 아무리 for에 비해 코드를 짧게 쓸 수 있고 가독성이 좋더라도

 

for문의 강력한 break, continue 기능을 대체하기 어렵기 때문에 break와 continue를 사용해야 하는 상황에는 for문을 쓰는 것이 적절하다.