JS는 기본적으로 함수 지향 언어로 사용할 수 있다
그리고 지금 제시한 closure라는 개념은 함수에 적용이 가능하다
그렇다면 대체 closure란 무엇을 의미하는 것일까?
함수의 특성부터 알아보자
함수와 가비지 컬렉터
함수는 실행이 되는 순간 변수가 생성되고
실행이 끝나는 순간 삭제가 된다고 언급했었다
이는 메모리가 삭제 즉, 가비지 컬렉터에 들어갔다고 볼 수 있다
따라서 이러한 단점을 삭제하기 위한 방법으로 closure를 제시한다
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
이러한 코드를 살펴보자
이때 counter라는 변수엔 return문이 선언된 function이 할당된다
그렇다는 것은 counter가 이제부터 return된 function을 참조하겠다는 의미와 같다
원래 보통의 함수였다면 makeCounter는 실행이 끝마쳤다면 메모리에서 사라지는 것이 관행이다
하지만 이는 root가 makeCounter내부의 function을 참조하고 있다
따라서 가비지 컬렉터에 수집되지 않음을 의미한다
closure의 특징
앞서 보았던 예시를 다시 한 번 뜯어보자
makeCounter라는 함수 내부에 생성된 함수를 다른 변수에 할당하고 이를 인수로 넘겨주기도 가능하다
또한 생성된 곳이 아니더라도 함수를 호출할 수도 있다
그리고 전에 배웠던 scope chaining에 의해 함수 내부에서 외부의 변수에도 접근이 가능하다
만약 counter라는 함수를 counter1, counter2, counter3과 같이 여러 개의 함수를 만들어 그 곳에 makeCounter()를 할당한다고 가정해보자
이때 각각의 counter1,2,3은 함수가 되었다
그렇다면 각각 count라는 값을 공유할까?
그렇지 않다

그 이유는 바로 함수가 각각 자신만의 scope를 구축하기 때문이다
따라서 그 scope안에 있는 변수는 서로 공유하지 않기 때문에 counter의 값도 공유하지 않는 것이다
이러한 이유로 전역의 오염을 방지하고 캡슐화를 하여 외부의 변수에도 접근할 수 있는 함수가 된다
closure의 단점
closure의 장점이 바로 함수가 실행이 종료되어도 메모리에서 삭제되지 않는다고 했었다
하지만 이것은 역으로 단점으로 생각할 수도 있다
지워지지 않는다는 것은 계속해서 자리를 차지하고 있다는 의미이기도 하다
지워지지 않는 lexical scope를 생성하기에 callstack에 부하를 주는 것이다
(사실 이는 사용자의 입장에선 큰 체감이 되진 않는다)
closure의 사용
우리는 전에 함수를 encapsulization을 한다는 IIFE패턴에 대해 언급했던 기억이 있다
그리고 오늘 closure를 얘기하면서 똑같은 단어를 언급했었다
바로 캡슐화(encapsulization)와 전역의 오염 방지이다
const handleClick = (() => {
let clicked = false;
return () => {
if (!clicked) {
document.body.style.backgroundColor = 'dodgerblue';
} else {
document.body.style.backgroundColor = 'white';
}
clicked = !clicked;
};
})();
button.addEventListener('click', handleClick);
위의 예제와 비슷한 코드이다
하지만 차이점이 있다
바로 IIFE패턴(즉시 실행 함수)으로 작동한다는 점이다
작동에서의 차이점을 살펴보자
우선 이는 button이라는 태그에 click이라는 이벤트를 추가하는 코드이다
하지만 실행되는 이벤트가 handleClick으로 함수 전체가 할당되었다
그렇다면 내부에 존재하는 return이 실행이 안되는 것은 아닌가?
그렇지 않다 왜냐하면 IIFE패턴으로 작성된 함수는 선언과 동시에 실행이 되기 때문이다
따라서 handleClick으로 할당받은 부분은 사실 return에 있는 부분을 할당받는 것과 동일한 동작을 하고 있다
다음은 react의 얘기를 잠깐 해보자면
react에서는 useState라는 method를 상당히 많이 사용한다
useState의 사용법은 간단하다
[number,setNumber] = useState('');
와 같은 형식으로 사용한다
이때 number는 값을 호출하는 함수를, setNumber는 값을 할당하는 함수를 담당하고 있다
이를 closure에 대입하여 살펴보자
function useState(init) {
let value = init;
function read() {
return value;
}
function write(newValue) {
value = newValue;
}
return [read, write];
}
이제 useState가 closure같다는 감이 온다
closure는 중요하지만 많은 사람들이 본인도 모르게 사용하고 closure라고 인식하지 못하는 경우가 많다고 한다
하지만 closure가 무엇인지 모르는 것보단 알고 어떻게 동작하는지 안다면 설계적인 측면에서 좀 더 수월할거라 예상한다
?.
옵셔널 체이닝은 전에 잠깐 언급했던 기억이 있다
오늘은 조금 더 자세히 알아보자
우선 사용법은 ?. 처럼 표기하고 작동은 ?? , || , &&과 비슷하게 작동한다
탐색 과정에서 만약 null혹은undefined값이 있다면 탐색을 정지하고 undefined를 반환한다
이는 변수 뿐만 아니라 method 앞에도 사용이 가능하다
const user = {
age:20,
height:160,
}
console.log(user.userA.age); // TypeError
console.log(user.userA?.age); // undefined
user 내부에 userA라는 객체가 있는지만 조사하게 되면 JS의 특성상 undefined를 출력한다
하지만 user내부에 userA라는 객체 내부에 age가 있는지를 조사하면 TypeError를 throw한다
이때 userA 내부에 age가 없다면 즉, null이라면 undefined를 출력하도록 옵셔널 체이닝을 이용한 것이다
또한 optional chaining은 method에도 사용이 가능하다
const portableFan = {
maker: 'fromB',
brand: 'FD221',
type: 'neckband',
photo: {
static: 'https://bit.ly/3OS50UD',
animate: 'https://bit.ly/3P8646q',
},
getFullName() {
return `${this.brand}, ${this.maker}`;
},
};
const fullName = portableFan.getFullName?.();
이때 portableFan이라는 객체 내부에 getFullName이라는 method가 존재하므로 fullName에는 해당 return값이 할당된다
하지만 getFullName이라는 메서드를 지우고 실행하게 되면 fullName에는 undefined가 할당된다


동기와 비동기란?
동기(sync)는 웹에서 코드의 순서대로 진행하며 전 단계가 끝나고 다음 단계를 실행하는 것을 의미한다
그에 반해 비동기(async)는 순서대로 진행하되 만약 비동기가 선언된 곳이 있다면 그것을 병렬적으로 실행하는 것을 의미한다
예를 들어 10초가 걸리는 작업1, 그리고 5초가 걸리는 작업2를 수행하고 작업3을 수행하려고 한다
그렇다면 작업3은 실행 후 15초 후에 작업을 시작할 수 있다
만약 각각을 비동기 처리를 한다면 10초만에 모든 작업을 끝낼 수 있다는 의미이다
기본적으로 JS는 동기적으로 작동하지만 이는 어떤 부분에 있어서는 비효율적일 수 있다
단일 스레드
JS는 단일 스레드이다
이는 하나의 메인 스레드에서 호출된 함수를 callstack에 저장되는 형식으로 작동한다
또한 callstack도 하나이다
따라서 JS는 한 번에 하나의 작업만을 수행할 수 있다
그렇다면 어떻게 비동기처리가 가능한 것일까?
바로 비동기 처리는 웹 엔진에 존재하는 web api에 의존하는 방식을 채택하고 있다
비동기 처리가 일어나는 과정은 다음과 같다
setTimeout
비동기의 가장 기본적인 method인 setTimeout이다
const t = setTimeout(() => {
console.log('time out');
}, 5000);
이는 5초 후에 'time out'이라는 string을 출력한다
이때 5000은 5000ms로 5s와 같다
이렇게 보면 비동기 정말 편하고 간단하다고 생각할 수 있지만
setTimeout의 단점에 대해 살펴보자
우선 setTimeout은 시간을 보장해주지 못한다
우선 비동기는 동기적으로 실행하다가 비동기가 선언된 곳을 만나면 비동기로 처리한다고 했었다
예를 들어 비동기로 선언된 곳 앞에서 재귀함수에서 언급했었던 memoization을 사용하지 않은 fibonacci(45)를 실행했다고 가정하자
그리고 fibonacci(45)가 대략 1분정도 걸린다고 가정해보자
그렇다면 비동기 함수는 1분과 설정한 시간을 더한 만큼 사용해야 본문이 실행된다
따라서 이를 해결하기 위해 promise와 async await를 사용한다
참고로 t에는 현재 setTimeout이 실행되기 위한 고유의 ID가 할당된다
이때 setTimeout을 중지시키고 싶다면 clearTimeout(t)를 사용하면 된다
두 번째 단점으로는 순서에 민감하다는 것이다
오류가 발생할 확률이 높다setTime으로 인해 tag생성이 지연되고 이후 나오는 attach event가 방향을 잃어버릴 수 있기 때문이다setInterval
setInterval은 setTimeout과 유사하게 동작한다
이는 설정한 시간을 간격으로 동일한 작업을 반복하는 method이다
주로 애니메이션을 구현할때 많이 사용된다
setInterval이 수행 중일때 다른 탭을 보고 있으면 잠시 일시정지한다
requestAnimationFrame이 setInterval과 같은 역할을 한다
하지만 차이가 있다면 requestAnimationFrame은 term에 대한 default값이 있다
이는 개인의 화면 주사율의 차이에 따라 달라진다
webGL과 Three.js에서 주로 사용된다
하지만 직접 할당하고 값을 설정하는 방식이 불편해 보통 gsap을 많이 사용하는 추세이다
숫자형은 일반 숫자형과 BigInt로 나뉜다
단위 표현
단위가 큰 함수를 입력하기 위해선 두 가지 방법이 존재한다
const billion = 1_000_000_000;
const billion = 1e9;
자릿수를 나타내기 위해선 ,이 아닌 _로 구분해야 한다
1e9는 1뒤에 0을 9개 붙인다는 의미이다
소수점을 나타내는 것도 비슷하다
const decimal = 1e-3;
소수점으로 3칸만큼 내리겠다는 의미이다
따라서 decimal에는 0.001이 할당된다
진수 변환
2진수, 8진수, 16진수로 변환하는 것이 가능하다
이는 parseInt 혹은 toString으로 가능하다
const a = 10;
const b = a.toString(2);
const c = parseInt(a,8);
const d = a.toString(16);

method 사용
숫자형에서 method를 사용하는 방법은 다른 변수에 할당하여 사용이 가능하다
하지만 숫자 자체에 method를 사용할땐 어떻게 해야할까?
const a = 11.toString(2);
const a = 11..toString(2);
const b = (11).toString(8);

만약 숫자 자체에 method를 사용할때 . 만 사용하면 인식하지 못한다
이는 소수로 인식할 가능성이 있기 때문이다 따라서 .. 을 사용하여 method를 실행할 수 있다
더욱 간단하게 사용하려면 ()로 묶어서 사용할 수도 있다
어림수
toFixed()
이를 활용하여 소수점을 몇번째까지 나타낼지 정할 수 있다
const a = 1/3;
console.log(a.toFixed(4));

주의점
만약 소수점의 합을 계산할때는 주의해야하는 것이 있다
console.log(0.1+0.2);
이것의 결과는 누가 봐도 0.3이 나올거라고 생각한다
하지만 출력 결과는 상당히 의외다

이는 메모리와 비트와 연관이 있다 이후 더 찾아보는 것이 좋다
또한 숫자형 NaN은 서로 다른 곳에서 선언되면 둘이 같은지 === 을 사용하여 확인할 수 없다
const a = NaN;
const b = NaN;
console.log(a===b);

따라서 둘 다 NaN인지 판단하려면 두 가지 방법이 있다
1. isNaN활용 -> 해당 값이 NaN인지를 판단
2. Object.is(a,b) -> a와b가 일치하는지 확인
하지만 Object.is는 배열과 객체를 비교할때 메모리도 비교하기 때문에 주의해야한다