-
클로저에 대해 알아보기 with useStateFrontEnd/JavaScript 2024. 10. 9. 16:19
클로저란 실행 컨텍스트와 렉시컬 스코프의 조합으로 함수가 선언 되었을 당시에 환경을 기억하여 상위 스코프내의 변수에 접근 할 수 있게 해주는 문법이다.
아래 함수를 실행해보면 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