(번역) 이상한 자바스크립트: 왜 every()는 빈 배열에 true를 반환할까?

sehyun hwang·2023년 9월 19일
26

FE 번역글

목록 보기
24/30
post-thumbnail

테스트할 값이 없는데 어떻게 조건을 만족시킬 수 있을까요?

원문 : https://humanwhocodes.com/blog/2023/09/javascript-wtf-why-does-every-return-true-for-empty-array/

자바스크립트 언어의 코어는 방대하기 때문에 특정 부분의 동작을 오해하기 쉽습니다. 저는 최근에 every() 메서드를 사용하는 코드를 리팩토링하면서 실제로 그 안의 상세한 로직을 이해하지 못하고 있다는 사실을 깨달았습니다. 제 생각으로는, every()true를 반환하기 위해서는 콜백 함수가 반드시 호출되어 true를 반환해야 한다고 가정했지만 실제로는 그렇지 않았습니다. 콜백 함수와 관계없이 every()는 빈 배열에 대해 true를 반환했습니다. 왜냐하면 콜백 함수가 호출되지 않기 때문입니다. 다음 예제를 살펴보겠습니다.

function isNumber(value) {
    return typeof value === "number";
}

[1].every(isNumber);            // true
["1"].every(isNumber);          // false
[1, 2, 3].every(isNumber);      // true
[1, "2", 3].every(isNumber);    // false
[].every(isNumber);             // true

이 예제의 각 케이스에서 every() 호출 시 배열의 각 아이템이 숫자인지를 확인합니다. 초반 4개의 케이스는 every()가 예상한 결과를 도출하기 때문에 직관적입니다. 이제 아래의 예제를 고려해 보세요.

[].every(() => true);           // true
[].every(() => false);          // true

좀 더 놀랍지 않나요? truefalse를 반환하는 콜백의 결과가 동일합니다. 이런 결과가 발생할 수 있는 유일한 이유는 콜백이 호출되지 않고 every()의 기본값이 true라고 가정할 때입니다. 그런데 콜백 함수를 실행할 값이 없을 때도 왜 every()는 빈 배열에 대해 true를 반환할까요?

그 이유를 이해하기 위해서는 이 메서드를 설명한 명세서를 자세히 들여다볼 필요가 있습니다.

every() 구현하기

ECMA-262는 대략 아래와 같은 자바스크립트 코드로 해석되는 Array.prototype.every() 알고리즘을 정의합니다.

Array.prototype.every = function(callbackfn, thisArg) {

    const O = this;
    const len = O.length;

    if (typeof callbackfn !== "function") {
        throw new TypeError("Callback isn't callable");
    }

    let k = 0;

    while (k < len) {
        const Pk = String(k);
        const kPresent = O.hasOwnProperty(Pk);

        if (kPresent) {
            const kValue = O[Pk];
            const testResult = Boolean(callbackfn.call(thisArg, kValue, k, O));

            if (testResult === false) {
                return false;
            }
        }

        k = k + 1;
    }

    return true;
};

위의 코드로부터 every()는 결과를 true로 가정하고 배열 내의 어떤 아이템이 콜백 함수에서 false를 반환할 때만 false를 반환하는 것을 알 수 있습니다. 만약 배열 내에 아이템이 없다면, 콜백 함수를 실행할 기회가 없고 따라서 false를 반환할 방법도 없는 것입니다.

이제 질문은, 왜 every()가 이런 식으로 동작할까요?

수학과 자바스크립트에서 "전체" 양화사 ("for all" quantifier)

MDN 페이지에서는 every()가 빈 배열에 대해 true를 반환하는 이유에 대한 답을 제공합니다.

every는 수학에서 "전체" 양화사처럼 동작합니다. 특히, 빈 배열의 경우 true를 반환합니다. (이는 공집합의 모든 원소가 주어진 어떠한 조건이든 모두 만족하는 공허참입니다.)

공허참은 만약 전제(antecedent)라 불리는 주어진 조건을 만족시킬 수 없는 경우 (즉, 주어진 조건이 참이 아닐 때) 어떤 것이 참임을 의미하는 수학적 개념입니다. 이를 자바스크립트 용어로 해석해보면, 콜백을 호출할 방법이 없기 때문에 공집합에 대해 every()true를 반환합니다. 콜백은 테스트할 조건을 나타내고, 만약 배열에 값이 없어서 실행되지 못한다면, every()는 반드시 true를 반환합니다.

"전체" 양화사는 데이터 집합에 대해 추론할 수 있게 해주는 보편 양화(universal quantification)라는 큰 수학적 주제의 일부입니다. 특히 형식화 배열(typed array)에서 수학적 계산을 수행하는데 있어 자바스크립트 배열의 중요성을 고려할 때, 이러한 연산을 내부적으로 지원하는 것은 합리적입니다. 그리고 every()가 유일한 경우는 아닙니다.

역자주: 양화사(quantifier)란 술어 논리에서 특정 술어를 만족하는 대상이 주어진 논의 영역에 얼마나 존재하는지를 알려주는 의미를 갖습니다. 크게 존재 양화사(existential quantifier)와 보편 양화사(universal quantifier) 두 종류로 나뉩니다.

수학과 자바스크립트에서 "존재" 양화사 ("there exists" quantifier)

자바스크립트 some() 메서드는 존재 양화(existential quantification)의 "존재" 양화사를 구현한 것입니다 (영어 표현으로 "존재"는 "there exists", 또는 가끔 "exists", "for some"으로 나타냅니다). "존재" 양화사는 어떠한 빈 집합에 대해서도 결과가 거짓임을 나타냅니다. 따라서 some() 메서드는 빈 집합에 대해 false를 반환하고 콜백 역시 실행하지 않습니다. 아래에 예제가 있습니다.

function isNumber(value) {
    return typeof value === "number";
}

[1].some(isNumber);            // true
["1"].some(isNumber);          // false
[1, 2, 3].some(isNumber);      // true
[1, "2", 3].some(isNumber);    // true
[].some(isNumber);             // false
[].some(() => true);           // false
[].some(() => false);          // false

다른 언어에서의 양화사

컬렉션이나 이터러블에 대해 양화사 메서드를 구현한 프로그래밍 언어는 자바스크립트뿐만이 아닙니다.

  • 파이썬: all() 함수가 "전체"를 구현하고, any() 함수가 "존재"를 구현했습니다.
  • 러스트: Iterator:all()메서드가 "전체"를 구현하고, any() 함수가 "존재"를 구현했습니다.

따라서 자바스크립트도 every()some()에 대해 마찬가지입니다.

"전체" every()의 의미

every()의 동작이 직관적이지 않다고 생각하는지 여부는 논쟁의 여지가 있습니다. 하지만 이런 생각과 관계없이 에러를 방지하기 위해서는 every()의 "전체" 특성을 염두에 두어야 합니다. 요약하자면, 만약 every() 또는 비어있을 수 있는 배열을 사용하려면, 미리 명시적인 검사를 추가해야 합니다. 예를 들어, 만약 숫자 배열에 의존하는 연산이 있고 빈 배열에 대해서 연산이 실패해야 한다면, every()를 사용하기 전에 배열이 비었는지를 확인해야 합니다.

function doSomethingWithNumbers(numbers) {

    // 먼저 길이를 확인합니다
    if (numbers.length === 0) {
        throw new TypeError("Numbers array is empty; this method requires at least one number.");
    }

    // 그리고서 every()로 확인합니다
    if (numbers.every(isNumber)) {
        operationRequiringNonEmptyArray(numbers);
    }

}

다시 강조하면, 이는 비어있을 때 연산에 사용하면 안 되는 배열이 있을 때만 중요합니다. 그렇지 않은 경우라면 추가적인 확인이 없어도 됩니다.

결론

처음에는 빈 배열에 대한 every()의 동작에 놀랐지만, 연산의 더 넓은 맥락을 이해하고 이러한 기능이 다른 언어에도 확장된 것을 확인하면서 납득할 수 있었습니다. 만약 여러분도 이러한 동작 때문에 혼란스럽다면, every()를 마주할 때 생각하는 방법을 바꿔보는 걸 제안드립니다. every()를 "배열의 모든 아이템이 조건을 만족하는가?"로 읽는 대신, "배열에 조건을 만족하지 않는 아이템이 있는가?"로 읽어보십시오. 이러한 사고의 전환은 앞으로 자바스크립트 코드의 오류를 방지하는 데 도움이 될 것입니다.

마스토돈과 트위터를 통해 더 많은 정보를 제공해주신 Dr. Axel Rauschmayer, Bart Louwers, Naman, Ronny Haase, Alexey Raspopov, Ivan, 그리고 David Thomas께 감사의 인사를 전합니다.

각주

  1. ECMA-262: Array.prototype.every(callbackfn, [thisArg])
  2. MDN: Array.prototype.every()
  3. 공허참
  4. 보편 양화
  5. ECMA-262: Array.prototype.some(callbackFn, [thisArg])
  6. 존재 양화
  7. 파이썬 all()
  8. 파이썬 any()
  9. 러스트 Iterator:all
  10. github: every가 빈 배열에서 true를 반환한다는 점을 강조하세요

7개의 댓글

comment-user-thumbnail
2023년 9월 21일

잘 읽었습니다.

답글 달기
comment-user-thumbnail
2023년 9월 21일

우와... 글 내용이 너무 좋아서 다시는 못 잊을 것 같아요 감사합니다

답글 달기
comment-user-thumbnail
2023년 9월 22일

GOOD

답글 달기
comment-user-thumbnail
2023년 9월 22일

좋은 글 잘 읽었습니다~!

답글 달기
comment-user-thumbnail
2023년 9월 26일

every를 깊게 파주셔서 감사합니다

답글 달기
comment-user-thumbnail
2023년 9월 27일

흥미롭네요! 잘 읽고 갑니다 :)

답글 달기
comment-user-thumbnail
2023년 10월 19일
답글 달기