Frontend

[자바스크립트 번역] 왜 every()는 빈 배열에서도 true 를 반환하는가?

ej503 2024. 7. 21. 19:52

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

 

JavaScript WTF: Why does every() return true for empty arrays? - Human Who Codes

How can a condition be satisified when there aren't any values to test?

humanwhocodes.com

 

JavaScript 언어는 특정 부분이 어떻게 작동하는지 오해하기 쉽습니다. 저는 최근에 every() 메서드를 사용하는 코드를 리팩토링하다가 그 이면의 논리를 실제로 이해하지 못하고 있다는 사실을 발견했습니다. 제 머깃속에서는 every() 가 참을 반환하려면 callback 함수가 호출되어 참을 반환해야 한다고 생각했지만 실제로는 그렇지 않았습니다. 빈 배열의 경우 callback 함수가 호출되지 않기 때문에 callback 함수가 무엇이든 상관없이 every() 는 참을 반환합니다. 다음을 고려하세요:

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() 를 호출하면 배열의 각 항목이 숫자인지 확인합니다. 처음 네번의 호출은 매우 간단하며, every() 는 예상되는 결과를 생성합니다. 이제 다음 예제를 살펴보겠습니다:

 

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


더 놀라운 사실은 참 또는 거짓을 반환하는 callback 의 결과가 동일하다는 점입니다. 이런 일이 발생할 수 있는 유일한 이유는 callback 이 호출되지 않고 있고 every() 의 기본값이 참인 경우에만 가능합니다. 그렇다면 callback 함수를 실행할 값이 없는데 왜 빈 배열이 every() 에 대해 참을 반환할까요? 


그 이유를 이해하려면 명세서에서 이 메서드를 어떻게 설명하는지 살펴보는 것이 중요합니다.

 

Implementing 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()는 결과가 참이라고 가정하고 callback 함수가 배열의 모든 항목에서 거짓을 반환하는 경우에만 거짓을 반환한다는 것을 알 수 있습니다. 배열에 항목이 없으면 콜백 함수를 실행할 기회가 없으므로 메서드가 거짓을 반환할 방법이 없습니다.

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

MDN 페이지2에서 빈 배열에 대해 every()가 참을 반환하는 이유에 대한 답을 찾을 수 있습니다:

every는 수학의 “for all” 한정자처럼 작동합니다. 특히 빈 배열의 경우 참을 반환합니다. (빈 집합의 모든 요소가 주어진 조건을 만족하는 것은 공허참입니다.)

 

공허 진리란 주어진 조건(선행 조건이라고 함)을 만족시킬 수 없는 경우(즉, 주어진 조건이 참이 아닌 경우) 어떤 것이 참임을 의미하는 수학적 개념입니다. 이를 자바스크립트 용어로 다시 설명하자면, 콜백을 호출할 방법이 없기 때문에 빈 집합의 경우 every()는 참을 반환합니다. 콜백은 테스트할 조건을 나타내며, 배열에 값이 없어 실행할 수 없는 경우 every()는 참을 반환해야 합니다.

“for all” 은 데이터 집합을 추론할 수 있게 해주는 수학의 보편적 수량화라는 큰 주제의 일부입니다. 특히 타입이 지정된 배열에서 수학적 계산을 수행하는 데 있어 자바스크립트 배열의 중요성을 고려할 때 이러한 연산에 대한 기본 지원은 당연한 일입니다. 그리고 every()가 유일한 예는 아닙니다.

 

The "there exists" quantifier in mathematics and JavaScript

 

JavaScript some() 메서드는 실존적 수량화에서 “exists” 수량화자를 구현합니다(“there exists”는 “exists” 또는 “for some”이라고도 함). “exists” 한정자는 빈 집합에 대해 결과가 거짓임을 나타냅니다. 따라서 일부() 메서드는 빈 집합에 대해 거짓을 반환하고 콜백도 실행하지 않습니다. 다음은 몇 가지 예시입니다:

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

 

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

Python: all() 함수는 “for all”을 구현하는 반면 any() 함수는 “there exists”를 구현합니다.
Rust: Iterator::all() 메서드는 “for all”을 구현하는 반면, any() 함수는 “there exists”을 구현합니다.
따라서 JavaScript는 every() 및 some()과 잘 어울립니다.

 

every()의 동작이 직관적이지 않다고 생각하는지 여부는 논쟁의 여지가 있습니다. 그러나 어떤 의견을 가지고 있든 오류를 방지하려면 every()의 “모든 것에 대한” 특성을 알고 있어야 합니다. 요컨대, every() 또는 비어 있을 수 있는 배열을 사용하는 경우 미리 명시적인 검사를 추가해야 합니다. 예를 들어 숫자 배열에 의존하는 연산이 있는데 배열이 비어 있으면 실패하는 경우, every()를 사용하기 전에 배열이 비어 있는지 확인해야 합니다:

 

function doSomethingWithNumbers(numbers) {

    // first check the length
    if (numbers.length === 0) {
        throw new TypeError("Numbers array is empty; this method requires at least one number.");
    }

    // now check with every()
    if (numbers.every(isNumber)) {
        operationRequiringNonEmptyArray(numbers);
    }

}

 


결론
빈 배열에서 every()의 동작에 놀랐지만, 연산의 더 큰 맥락과 이 기능이 여러 언어에 걸쳐 확산된 것을 이해하면 이해가 됩니다. 여러분도 이 동작에 혼란스러웠다면, every() 호출을 접할 때 읽는 방식을 바꾸는 것이 좋습니다. “이 배열의 모든 항목이 이 조건과 일치하는가?"라고 읽는 대신 ‘이 배열에 이 조건과 일치하지 않는 항목이 있는가?’라고 읽어보세요. 이러한 사고의 전환은 앞으로 자바스크립트 코드에서 오류를 방지하는 데 도움이 될 수 있습니다.