원티드에서 주관한 프리온보딩 프론트엔드 챌린지
에 참여를 했는데 멘토님께서 useState
는 마법이 아니라며 closure
와 연관지어 설명해 주셨다. 여기에 흥미를 느껴 closure
를 공부해볼 겸 간단한 useState
를 만들어보자고 생각하게 됐다.
(간단하다고 했지만, 생각보다 오래 걸렸다...)
return
될 때 내부 함수가 지역 변수를 참조 한다면 외부 함수가 return
되더라도 외부 함수의 지역 변수는 사라지지 않고 내부 함수에서 계속해서 참조할 수 있다. function makeAdder(x) {
var y = 1;
return function(z) {
y = 100;
return x + y + z;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
//클로저에 x와 y의 환경이 저장됨
console.log(add5(2)); // 107 (x:5 + y:100 + z:2)
console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
//함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산
npm create vite
위 명령어로 Vanilla Javascript
템플릿을 만들고 리액트와 비슷한 폴더 구조를 구성했다.
📦custom-usestate-hook
┣ 📂src
┃ ┣ 📂components
┃ ┃ ┗ 📜counter.js
┃ ┣ 📂hooks
┃ ┃ ┗ 📜useState.js
┃ ┣ 📂router
┃ ┃ ┗ 📜render.js
┃ ┣ 📜main.js
┃ ┗ 📜style.css
┣ 📜.gitignore
┣ 📜favicon.svg
┣ 📜index.html
┣ 📜package.json
┣ 📜README.md
┗ 📜yarn.lock
우선 useState
에 대해 생각해보면 state
가 변경될 때 마다 리액트
는 re-render
를 한다. 그래서 우선 render
함수를 만들었다.
import Counter from "../components/Counter";
export const render = () => {
const app = document.querySelector("#app");
app.innerHTML = `
<div>
<div>${Counter()}</div>
</div>
`;
};
useState
를 만들고 간단히 테스트 해보기 위해 Counter
컴포넌트를 만들었다.
import useState from "../hooks/useState";
const Counter = () => {
const [count, setCount] = useState(0);
window.increment = () => setCount(count + 1);
window.decrement = () => setCount(count - 1);
window.reset = () => setCount(0);
return `
<div>
<strong> count: ${count} </strong>
<button> + </button>
<button> 초기화 </button>
<button> - </button>
</div>
`;
};
export default Counter;
useState
의 경우 state
변경 시 렌더링이 발생하기 때문에 아래와 같은 코드의 형태를 띄고 있을거라고 생각했다.
import { render } from "../router/render";
const useState = (initialState) => {
let state = initialState;
const setState = (newState) => {
state = newState;
render(); // setState로 state변경시 렌더링 발생
};
return [state, setState];
};
export default useState;
그런데 위의 코드는 버튼을 아무리 클릭해도 값이 변화하지 않는다.(이것을 해결하는데 오래걸렸다..)
setCount
가 실행될 때마다 re-render
가 발생하고 useState
내부의 state
는 결국 initialState
로 초기화 된다.이를 해결하기 위해 구글링을 했는데 useState
의 경우 state
를 useState
내부가 아닌 외부에서 관리해야 한다고 한다.
import { render } from "../router/render";
let state = undefined;
const useState = (initialState) => {
// state가 비어있을 경우에만 initialState로 초기화
if (state === undefined) {
state = initialState;
}
const setState = (newState) => {
state = newState;
render();
};
return [state, setState];
};
export default useState;
된다!
위의 코드는 잘 작동하는 것 처럼 보이지만 사실 큰 문제가 하나 남아있다. 컴포넌트
를 여러개 만들고 useState
를 여러번 호출 하게 되면 모든 컴포넌트
는 모든 state
를 공유하게 된다.
어찌보면 당연한 이야기다. state
는 useState
안에서 하나의 변수로 관리하기 때문이다.
이 현상을 해결하기 위해 state
를 Array
와 key
값을 통해 관리한다고 한다. 이는 언젠가 해결하고 다시 포스팅하기로 했다.