클로저란 실행 컨텍스트와 렉시컬 스코프의 조합으로 함수가 선언 되었을 당시에 환경을 기억하여 상위 스코프내의 변수에 접근 할 수 있게 해주는 문법이다.
아래 함수를 실행해보면 10이 출력되는 것을 통해 inner 함수가 상위 스코프인 outer의 _var를 참조하는 것을 알 수 있다.
이는 클로저에 의해 접근 가능한 것.
const outer = () => {
let _var = 10;
const inner = () => console.log(_var);
inner();
};
outer(); // 10
아래 함수를 예측해보면 어떻게 출력될까
const outer = () => {
let _var = 10;
const setVar = (newVal) => {
_var = newVal;
};
return [_var, setVar];
};
const [state, setState] = outer();
console.log(state);
setState(30);
console.log(state);
setState에 의해 값이 변경 되었으니 10, 30이 출력될 것으로 예상되지만 결과값은 10 10으로 값에 변화가 없다.
setVar 함수 내부에서 콘솔을 통해 찍어보면 10, 새로운 변수 할당: 30 ,10 이 출력된다.
const outer = () => {
let _var = 10;
const setVar = (newVal) => {
_var = newVal;
console.log('새로운 변수 할당:', _var);
};
return [_var, setVar];
};
const [state, setState] = outer();
console.log(state);
setState(30);
console.log(state);
setState는 제대로 동작했는데 state는 왜 계쏙 10을 출력하는 이유는 outer가 실행되면서 복사 된 _var는 10으로 고정 되어 있기 때문이다.
이것을 원하던 로직으로 해결하기 위해선 값을 복사하는 것이 아닌 호출할 때마다 값을 불러오는 get 함수를 짜면 된다.
const outer = () => {
let _var = 10;
const getVar = () => _var;
const setVar = (newVal) => {
_var = newVal;
console.log('새로운 변수 할당', _var);
};
return [getVar, setVar];
};
const [state, setState] = outer();
console.log(state());
setState(30);
console.log(state());
이렇게 작성하면 state를 실행할 때마다 외부 함수에서 기억하고 있는 var의 메모리에 접근한 뒤 그 안에 담겨진 값을 그때그때마다 가져와준다.
그렇다면 아래 코드의 결과는 어떻게 될까
const ZeroJin = (function () {
let state;
function useState(initialValue) {
if (state === undefined) {
state = initialValue;
}
const setState = (newValue) => {
state = newValue;
render(); // 상태 변경 시 리렌더링
};
return [state, setState];
}
function render() {
Counter();
}
return { useState, render };
})();
// 사용 예시
function Counter() {
const [count, setCount] = ZeroJin.useState(0);
console.log(`${stage} 단계의 현재 count 값:`, count);
return {
increment: () => setCount(count + 1),
decrement: () => setCount(count - 1),
getCount: () => count,
};
}
let stage = 0;
// 초기 렌더링
ZeroJin.render();
// 카운터 조작
const counter = Counter();
stage++;
counter.increment();
stage++;
counter.increment();
stage++;
counter.decrement();
//
console.log('Final count:', counter.getCount());
결과는 0,1,1,-1이다.
usetState의 반환 값을 const getState = () => state 로 반환하면 state원하는 대로 적용되어 0,1,2,1로 원하는 로직대로 출력 됨.
이 때 아래와 같이 각각의 count와 text를 선언했다면 어떻게 될까
function Counter() {
const [count, setCount] = ZeroJin.useState(0);
const [text, setText] = ZeroJin.useState('initial Text');
console.group(stage);
console.log(`${stage} 단계의 현재 count 값:`, count());
console.log(`${stage} 단계의 현재 text 값:`, text());
return {
increment: () => setCount(count() + 1),
decrement: () => setCount(count() - 1),
getCount: () => count(),
};
}
// 0
// 0 단계의 현재 count 값: 0
// 0 단계의 현재 text 값: 0
// 0
// 0 단계의 현재 count 값: 0
// 0 단계의 현재 text 값: 0
// 1
// 1 단계의 현재 count 값: 1
// 1 단계의 현재 text 값: 1
// 2
// 2 단계의 현재 count 값: 2
// 2 단계의 현재 text 값: 2
// 3
// 3 단계의 현재 count 값: 1
// 3 단계의 현재 text 값: 1
// Final count: 1
text는 count와 같은 같은 값이 출력된다.
이러한 이유는 Zerojin이 즉시 실행 함수로 실행되면서 private한 공간을 만들면서 동일한 클로저를 공유하기 때문이다.
이 같은 점을 피하기 위해 state를 객체 또는 배열로 설정하는 방법이 있다. 실제로 리액트에서 훅 타입을 살펴보면 아래와 같다.
export type Hook = {
memoizedState: any, //훅의 현재 상태를 저장합니다.
baseState: any, //업데이트 큐를 처리하기 전의 초기 상태입니다.
baseQueue: Update<any, any> | null, //처리되지 않은 업데이트들의 큐입니다.
queue: any, //향후 처리될 업데이트들의 큐입니다.
next: Hook | null, //다음 훅을 가리키는 포인터입니다.
};
다음 훅을 가리키는 포인터를 통해 훅들은 연결 리스트로 이루어진 것을 유추할 수 있고 아래처럼 선언된 훅 state는
Hook1 (count) -> Hook2 (name) -> Hook3 (isActive) -> null 로 연결 된 것을 알 수 있다.
function MyComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('John');
const [isActive, setIsActive] = useState(true);
}
이를 통해 각각의 state들은 독립적으로 작동되어 dispatcher에 의해 값 변경은 공유되지 않는다.
(내가 작성한 코드에서 연결 리스트 형식으로 hook을 작성하고 싶었지만 논리적 부족함으로 배열로 작성해봄)
const ZeroJin = (function () {
let hooks = null;
let currentHook = 0;
function useState(initialValue) {
if (hooks === null) {
hooks = [];
}
const hookIndex = currentHook;
currentHook++;
if (hooks[hookIndex] === undefined) {
hooks[hookIndex] = initialValue;
}
const setState = (newValue) => {
hooks[hookIndex] = newValue;
render();
};
return [() => hooks[hookIndex], setState];
}
function render() {
currentHook = 0;
const result = Counter();
return result;
}
return { useState, render };
})();
// 사용 예시
function Counter() {
const [count, setCount] = ZeroJin.useState(0);
const [text, setText] = ZeroJin.useState('initial Text');
return {
increment: () => setCount(count() + 1),
decrement: () => setCount(count() - 1),
updateText: (newText) => setText(newText),
getCount: () => count(),
getText: () => text(),
};
}
let stage = 0;
// 초기 렌더링
let counter = ZeroJin.render();
stage++;
counter.increment();
counter.updateText('first');
console.group(`Stage ${stage}`);
console.log(counter.getCount(), counter.getText());
// 1 first
console.groupEnd();
stage++;
counter.increment();
counter.updateText('second');
console.group(`Stage ${stage}`);
console.log(counter.getCount(), counter.getText());
// 2 second
console.groupEnd();
stage++;
counter.decrement();
counter.updateText('third');
console.group(`Stage ${stage}`);
console.log(counter.getCount(), counter.getText());
// 1 third
console.groupEnd();
참고: https://github.com/ZacharyL2/mini-react/blob/master/src/mini-react.ts
'FrontEnd > JavaScript' 카테고리의 다른 글
브라우저 엔진의 작업 처리 우선순위 (feat. 마이크로 테스크 큐, 테스크 큐, 렌더 큐) (0) | 2024.10.07 |
---|---|
Security Vulnerability: SSRF(Server-Side Request Forgery in axios) (0) | 2024.08.14 |
babel 직접 설정해보기 (0) | 2024.04.06 |
webpack 직접 설정을 위한 기본 개념 (0) | 2024.04.05 |
내가 JSON Viewer를 짠 방식 (1) | 2023.11.04 |