본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.
내장 함수 eval
을 이용하면 문자열 형태의 코드를 실행할 수 있다.
let code = 'alert("hello")';
eval(code); // hello
길이가 긴 문자열 역시 코드로 사용가능하다. 이때 줄 바꿈, 함수 선언, 변수 등이 모두 포함될 수 있다. eval
을 실행한 결과값은 마지막 구문의 결과이다.
let value = eval('1+1');
console.log(value); // 2
let value = eval('let i = 0; i++');
console.log(value); // 1
비엄격 모드에서 사용되는 eval
은 현재 렉시컬 환경에서 실행된다. 따라서 외부 변수에 접근이 가능하다.
let a = 1;
function f() {
let a = 2;
eval('console.log(a)');
}
f(); // 2
그러나 엄격 모드(use strict
)에서 evel
은 자체 렉시컬 환경을 갖게된다. 따라서 내부에 선언된 함수와 변수는 외부에서 접근할 수 없다.
'use strict'
evel('let x = 5; function f() {}');
console.log(typeof x); // undefined
console.log(typeof f); // undefined
모던JS 에서 eval
은 더이상 잘 사용하지 않는다. 농담으로 eval is evil
이라는 문구가 있을 정도이다. 과거에는 자바스크립트에서 쓸 수 있는 기능이 단순했기 때문에 eval
을 통해 동적으로 처리해야 할 부분이 많았지만, 오늘날 자바스크립트에서는 대부분 이런 기능을 eval
을 사용하지 않고 대체할 수 있다.
또한 대표적인 문제점으로는 eval
을 사용할 때는 항상 외부 변수 접근 시 사이드 이펙트가 발생할 수 있다는 우려가 있다. 보통 프로덕션 환경에서 애플리케이션이 배포되면, 자바스크립트로 구현한 코드는 압축기(minifier
) 또는 번들러와 같은 도구에 의해 스크립트 크기를 작게 만든다. 이 과정에서 개발자가 선언한 변수명이 a
, b
와 같이 짧게 변환되는데, eval
로 감싼 코드에서 이러한 지역 변수에 접근할 수 있기 때문에 이러한 압축 방식은 eval
이 스크립트 내부에 있다면 안전하지 않다. 따라서 압축기는 eval
내부 코드에서 접근할 가능성이 있다고 판단되는 모든 변수의 이름은 변경하지 않는다. 즉 압축률에 부정적인 영향을 미치게 된다.
eval
내부에서 사용하는 외부 지역 변수는 코드 유지 보수를 더 어렵게 만들기도 하기 때문에 좋지 않은 프로그래밍 관습으로 취급된다. 이러한 문제를 해결하기 위해 가장 좋은 방법은 eval
을 가급적 사용하지 않는 것이지만, 그 외에 다음의 방법으로 어느정도 예방할 수 있다.
eval
로 감싼 코드에서 외부 변수를 사용하지 않는 경우, eval
대신 window.eval
을 호출let x = 1;
{
let x = 5;
// 이 경우 eval 내의 코드는 전역 스코프에서 실행됨
window.eval('console.log(x)'); // 1
}
eval
로 감싼 코드에서 지역 변수를 사용하는 경우에는 eval
대신 new Function
을 사용let f = new Function('a', 'console.log(a)');
f(5); // 5
new Function
은 이전 함수 챕터에서 살펴보았듯이 인수로 받은 문자열을 기반으로 전역 스코프에 새로운 함수를 만들어준다. 따라서 지역 변수에 접근할 수 없다. 만약 지역 변수에 접근하려면 위와 같이 인수를 통해 값을 전달받도록 하는 것이 훨씬 더 명확한 방법이다.
커링(Currying
)은 함수와 함께 사용할 수 있는 일종의 (고급)기술이다. 때문에 오직 자바스크립트에서만 사용할 수 있는 기법은 아니다.
커링에 대해서는 이전 함수 챕터에서 살펴본 부분적용(partial application
) 기법과 어느정도 궤를 같이 한다. 커링은 f(a, b, c)
처럼 단일 호출로 처리하는 함수를 f(a)(b)(c)
와 같이 각각의 인수가 호출 가능한 프로세스로 호출된 후 병합되도록 변환하는 기술이다. 흔히 부분적용과 커링은 함수형 프로그래밍의 기법으로 소개가 되기도 한다.
커링은 함수를 호출하지 않는것에 유의하자. 커링은 그저 변환만 할 뿐이다. 예제를 통해 커링이 무엇인지 살펴보도록 하자. 다음은 f(a, b)
처럼 두 개의 인수를 요구하는 함수를 f(a)(b)
형식으로 변환하는 커링 함수 구현 예제이다.
function curry (f) {
return function (a) {
return function (b) {
return f(a, b);
};
};
}
function sum (a, b) {
return a + b;
}
let curriedSum = curry(sum);
console.log( curriedSum(1)(2) ); // 3
예제에서 볼 수 있듯이 커링함수는 그저 두 개의 래퍼함수를 반환하는 매우 간단한 구조를 가지고 있다.
curry(f)
의 반환값은 function(a)
형태의 래퍼 함수curriedSum(1)
이 호출되면, 그 인수는 렉시컬 환경에 저장이 되고 다시 새로운 래퍼함수 function(b)
반환function(b)
에 인수를 2
로 하여 호출하며, 이때의 반환값은 기존 함수 sum
으로 넘겨져 호출커링 함수는 이처럼 직접 구현할 수도 있지만 보통 lodash
와 같은 라이브러리에서 사용하는 것을 선호한다. _.curry
의 경우 래퍼 함수를 반환할 때 함수가 보통 때처럼 또는 partial
적으로 호출하는 것을 모두 허용하는 더 진보된 커링을 제공하기 때문이다.
function sum (a, b) {
return a + b;
}
let curriedSum = _.curry(sum); // lodash 라이브러리
console.log( curriedSum(1, 2) ); // 3 (보통 함수처럼 호출)
console.log( curriedSum(1)(2) ); // 3 (partially 호출)
정보를 형식화하고 출력하는 로그 함수 log(date, importance, message)
가 있다고 가정해보자. 실제 프로젝트에서 이런 함수는 네트워크를 통해 로그를 보내는 것과 같은 다양한 기능을 제공할 것이다. 여기서는 콘솔창에 로깅을 출력하도록 해보자.
function log (date, importance, message) {
const hour = date.getHours();
const minute = date.getMinutes();
console.log(`[${hour}:${minute}] [${importance}] ${message}`);
}
log = _.curry(log);
log
함수를 구현함과 동시에 lodash
라이브러리를 이용해 커링을 적용했다. 이처럼 커링을 적용하더라도 본래 방식처럼 함수를 호출하면 정상적으로 잘 동작한다.
log(new Date(), "DEBUG", "some debug"); // log(a, b, c)
// 커링방식 호출
log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)
커링을 이용하여 다음과 같이 현재 시간으로 로그를 출력하는데 편리하도록 log
함수를 작성해서 사용할 수 있다.
// log의 시간값이 고정된 partial
let logNow = log(new Date());
logNow("INFO", "message"); // [HH:mm] INFO message
이것에 추가적으로 디버깅 로그를 관리할 수 있는 함수를 또 구현할 수 있다.
let debugNow = logNow("DEBUG");
debugNow("message"); // [HH:mm] DEBUG message
결과적으로 커링을 적용한 이후에 잃은 것은 없다. log
는 언제든지 보통 때처럼 동일하게 호출이 가능하다. 그 외에 기능을 각각 분리하여 전용 함수로 손쉽게 구현 후 사용이 가능하다는 장점이 있다.
다음은 다중 인수를 허용하는 '고급' 커리를 구현하는 방법이다. 재귀 호출을 이용하며 흐름을 이해한다면 그렇게 어렵지 않다.
function curry (func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function pass(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
function sum(a, b, c) {
return a + b + c;
}
let curriedSum = curry(sum);
console.log( curriedSum(1, 2, 3) ); // 보통과 같은 callable 방식
console.log( curriedSum(1)(2, 3) ); // 첫 번째 인수 커링
console.log( curriedSum(1)(2)(3) ); // 모두 커링
위 커링함수의 흐름은 다음과 같다. 먼저 래퍼함수를 반환할 때 if
로 인해 두 개의 분기점이 내부에 형성된다.
첫 번째 조건에 해당하는 경우는 함수가 보통 방식으로 호출되었을 경우를 말한다. 전달된 인수의 개수는 args.length
로 알 수 있고, 원본 함수 인수의 길이는 함수 역시 객체이기 때문에 내부 프로퍼티 속성 length
를 통해 둘을 비교할 수 있다.
만약 첫 번째 조건에 해당되지 않는다면 partial
이 적용되는 순간이다. 이때는 원본 함수가 호출되지 않고, pass
라는 새로운 래퍼함수를 반환한다. 그리고 pass
래퍼함수는 curried
를 이전 함수와 새로운 인수와 함께 다시 적용한다. 즉 재귀적으로 다시 curried
래퍼함수를 호출하고 있다. 따라서 전달된 인수의 개수가 첫 번째 조건을 만족할 때 까지 위의 로직을 반복하며 결과값을 반환하게 된다.
커링은 해당 함수가 고정된 개수의 인수를 가지도록 요구한다. 즉
f(...args)
같은 나머지 매개변수를 사용하는 함수는 위와 같은 방식으로 커리하기 힘들다.
앞서 많은 상황에서 메서드 호출 시 this
에 대한 정보를 잃어버리는 경우를 보았다. 다음의 예시를 한번 더 살펴보자.
let user = {
name: "John",
hi() { console.log(this.name); },
bye() { console.log('bye'); }
};
user.hi(); // John (의도대로 동작)
(user.name === 'John' ? user.hi : user.bye)(); // TypeError
마지막 줄에서는 조건부 연산자를 사용해 user.hi
또는 user.bye
함수 자체를 반환하고 있다. 그리고 반환이 끝나면 ()
를 통해 직접 호출하고 있다. 언뜻 보면 이 같은 동작은 정상적으로 수행될 것 같지만 위와 같이 에러 메시지가 발생하는 것을 볼 수 있다. 이는 this
에 undefined
가 할당되었기 때문이다. 그 원인을 파악하기 위해서는 obj.method()
호출 시 내부에서 어떤 일이 일어나는지 알아야 한다.
코드를 유심히 살펴본다면 obj.method()
에는 두 개의 연산이 있다는 점을 알 수 있다.
.
) 연산자는 객체 프로퍼티에 접근()
연산자는 접근한 프로퍼티(메서드)를 호출첫 번째 연산이후 두 번째 연산이 실행되기 때문에, 우리는 첫 번째 연산에서 얻은 this
의 정보가 두 번째 연산으로 전달된다는 것을 알 수 있다. 이때까지는 너무나 당연하게 받아들였지만, 이러한 작업이 어떻게 가능한 것일까?
당연하게도 두 연산을 각각 별도의 라인에 구현하면 this
의 정보는 소실된다. hi
는 함수 자체가 변수에 할당되고, 따라서 이는 독립적으로 동작하는 함수가 되기 때문에 this
가 undefined
를 가리키게 된다.
user.hi(); // John (정상 동작)
let hi = user.hi;
hi(); // TypeError: this = undefined
자바스크립트는 user.hi()
를 의도한대로 동작시키기 위해 일종의 속임수를 사용한다. .
연산자는 함수가 아닌 참조 타입(Reference Type) 값을 반환하게 한다.
참조 타입은 명세서에서만 사용되는 타입이기에 개발자가 실제로는 사용할 수 없다. 참조 타입에 속하는 값은 (base, name, strict)
가 조합된 형태를 가진다.
base
: 객체name
: 프로퍼티의 이름strict
: 엄격모드에서 true
따라서 user.hi
로 프로퍼티에 접근하면 함수가 아닌, 참조 타입 값을 반환한다. 엄격 모드에서는 다음과 같은 값이 반환될 것이다.
(user, "hi", true)
이런 참조 타입 값에 괄호 ()
를 붙여 호출하면 객체, 객체의 메서드와 연관된 모든 정보를 받게 된다. 따라서 이 정보를 기반으로 메서드 내에서 this = user
라는 정보를 결정하게 되어 정상적인 동작이 가능한 것이다.
이처럼 참조 타입은 내부에서 점(.
) 연산에서 알아낸 정보를 괄호 ()
로 전달하는 중개인 역할을 한다. 그러나 위에서 살펴본 바와 같이 점 연산 이외의 연산(할당 연산 등)은 참조 타입을 통째로 버리고 user.hi
와 같은 함수 값만 받아 전달하기 때문에 this
에 대한 정보가 소실된다.
점 연산자 외에도 프로퍼티를 접근할 때 사용하는 대괄호([]
) 연산 역시 참조 타입 값을 사용하여 this
값을 전달한다(obj[method]
). 다른 연산에서 이러한 문제는 함수 챕터에서 살펴본 바와 같이 func.bind
등 함수 내장 메서드를 이용해 this
에 대한 컨텍스트를 명시하는 것으로 해결이 가능하다.
BigInt
는 ES2020에서 도입된 최신 스펙이다. 해당 사이트에서 브라우저별 지원 여부를 파악할 수 있다.
C언어
나 JAVA
의 경우에는 숫자 타입이 int, long, float, double
등 다양한 형태로 정의되어 사용할 수 있다. 그러나 자바스크립트에서는 초반에 살펴본 것과 같이 오직 number
타입만으로 숫자를 구분한다.
자바스크립트의 숫자는 메모리에 IEEE-754
부동소수점 형식을 이용해 저장되는데, 이 형식은 부호/지수/가수
로 이루어져 있으며 자바스크립트에서는 총 32비트까지 표현이 가능하다. 이에 대한 자세한 설명은 위키백과를 참고하자.
자바스크립트에서 number
타입은 이처럼 정수 및 소수를 모두 아우르기 때문에 형변환이 결과값에 맞추어 알아서 이루어지는 경우가 많다. 대표적으로 정수형으로 선언하는 다른 언어에서는 5 / 2
의 결과값이 정수형 2
로 나누어 떨어지지만, 자바스크립트에서는 이러한 구분을 두지 않기 때문에 알아서 float
형 처럼 2.5
로 계산된다.
자바스크립트에서는 number
타입으로 표현가능한 수의 범위가 -(2^53-1)
부터 2^53-1
까지 표현이 가능하다. 하지만 이보다 큰 값을 표현해야 하는 경우에는 원시값인 Number
만으로는 불가능하다. 이 경우에 이 보다 큰 정수를 BigInt
를 이용하여 표현할 수 있다.
스펙에 의하면 BigInt
는 길이의 제약 없이 정수를 다룰 수 있게 해주는 숫자형이다. 그러나 메모리의 용량에 따른 제약이 있기 때문에 엄연히 말하면 표현할 수 있는 한계는 존재한다. 중요한 점은 기존 원시값 Number
보다 더 큰 수를 표현할 수 있다는 것이다.
정수 리터럴 끝에 n
을 붙이거나 함수 BigInt
를 호출하여 문자열이나 숫자를 가지고 BigInt
타입의 값을 만들 수 있다.
const bigint = 1234567890123456789012345678901234567890n;
const sameBitint = BigInt("1234567890123456789012345678901234567890");
const bigintFromNumber = BigInt(10); // 10n과 동일
BigInt
는 대개 일반 숫자와 큰 차이 없이 사용할 수 있다. 이때 Number
원시타입과는 다르게 항상 정수형을 반환하므로 연산 결과는 언제나 소수점 이하를 버린다.
console.log( 1n + 2n ); // 3n
console.log( 5n / 2n ); // 2n
또한 BigInt
타입의 숫자는 일반 숫자와 혼합하여 연산할 수 없다. 일반 숫자와 섞어서 사용해야 하는 경우에는 BigInt()
또는 Number()
를 사용해 명시적으로 형 변환을 해야한다.
console.log( 1n + 3 ); // Error
let bigint = 1n;
let number = 2;
console.log( bigint + BigInt(number) ); // 3n
console.log( Number(bigint) + number ); // 3
형 변환과 관련된 연산은 항상 내부적으로 조용히 동작한다. 따라서 절대 에러를 발생시키지 않는다. 하지만 BigInt
타입을 Number
로 변환할 때 너무 큰 값이라면 값의 일부가 잘려 나갈 수 있다는 점에 주의해야 한다.
또한 단항 덧셈 연산자의 경우는 BigInt
타입에 적용할 수 없다. 이는 혼란을 방지하기 위해서 문법적으로 지원을 막은 것이다.
let bigint = 1n;
console.log( Number(bigint) ); // 1
console.log( +bigint ); // Error
비교 연산자는 모두 BigInt
타입 숫자 그리고 일반 숫자와 혼합해서 정상적으로 사용할 수 있다.
console.log( 2n > 1n ); // true
console.log( 2n > 1 ); // true
그러나 비교하려는 대상의 값은 같고 타입이 서로 다른 경우에는 동등 연산자(==
)는 서로 같다고 판단하지만 일치 연산자(===
)는 서로 다르다고 판단한다.
console.log( 1 == 1n ); // true
console.log( 1 === 1n ); // flase
BitInt
타입의 숫자는 if
문 내부나 다른 논리 연산자와 함께 사용할 때 일반 숫자와 동일하게 행동한다. 즉 0n
은 falsy
로 판단하고 나머지는 모두 truthy
로 판단한다. 이는 ||
, &&
등의 논리 연산자를 적용할 때도 동일하다.
if (0n) {
// 절대 실행되지 않는 영역
}
console.log( 1n || 2 ); // 1n
console.log( 0n || 2 ); // 2
최신 스펙이기 때문에 구식 브라우저에서도 BigInt
를 지원하게끔 하려면 폴리필을 적용해야한다. 그러나 BigInt
는 폴리필을 구현하기가 여간 까다로운 것이 아니다. +
, -
등의 다양한 연산자들이 BigInt
타입 숫자와 일반 숫자에서 다른 결과를 보이기 때문이다.
동일한 결과를 출력하기 위해서는 폴리필에서 기존 코드를 분석하고 내장 연산자 모드를 관련 함수로 대체하는 등 상당한 노력이 필요하고, 자칫하면 성능적인 이슈가 발생할 수 있는 우려도 있다. 따라서 아직까지는 제대로 된 BigInt
관련 폴리필이 등장하지 않은 상황이다.
잘 알려진 폴리필은 없지만 JSBI
라이브러리의 개발자들이 대안을 제시한 바 있다. 이 라이브러리는 자체적으로 만든 방법을 사용해 큰 숫자를 구현한다. 순수 BigInt
대신 라이브러리에서 만든 숫자를 사용하는게 일종의 대안이 될 수 있다.