const [state, setState] = useState(initialValue);
요건 우리가 흔히들 사용하는 useState이다.
보통 useState는
1. 초기화
2. 상태저장
3. 상태 업데이트
요 3가지의 동작을 하는데 보통 우리는 얘가 저렇게 해주니까 그냥 써야지 ㅇㅇ 하고 사용하고 만다. 그래도 Hooks들은 React에서 만들어둔 함수이기 때문에 로직이 존재할 것이다. 지금부터 어떻게 돌아가는지 알아보자!
우선 useState의 동작을 알아보기 전에 React처럼 동작하도록 함수를 만들어준다.
const MyReact = {
render(Component) {
const Comp = Component(); // 컴포넌트실행
Comp.render(); // 컴포넌트 렌더링 실행
return Comp;
},
};
export default MyReact;
이후 useState를 구현해보자
//MyReact.mjs
let hooks = [], // hook을 관리하는 배열
currentHook = 0; // hook의 현재 인덱스
const useState = (initialValue) => {
hooks[currentHook] = hooks[currentHook] || initialValue; //현재 훅에 존재하는 값을 꺼내오던가 initialValue를 할당
const hookIndex = currentHook;
const setState = (newState) => {
if (typeof newState === "function") {
// 입력 받은 newState의 타입이 함수인지 확인하고 함수이면 함수형 업데이트 진행
hooks[hookIndex] = newState(hooks[hookIndex]);
} else {
hooks[hookIndex] = newState; // 아니라면 일반 상태 업데이트
}
};
return [hooks[currentHook++], setState]; //hooks에 존재하는 상태 및 다음 hook을 위한 인덱스 증가
};
주석에다가 모두 달아두긴 했지만 하나씩 뜯어보자.
우선 React에서는 Hook을 배열을 통해 관리한다. 이 아티클을 통해 확인 할 수 있다.
아무튼 hooks를 관리하는 배열과 hooks배열의 현재 인덱스를 따로 관리하고 현재 인덱스에 맞는 hook을 꺼내서 state를 가져오게 된다. setState는 입력값의 타입을 확인 후에 function 타입이면 함수형 업데이트를 통해 state를 업데이트 할 수 있게 하고 아니라면 보통의 업데이트를 진행하게 한다. return 시에는 hook을 관리하는 배열의 인덱스를 다음 훅을 위해 늘려주고 return을 하고 setState는 그대로 넘겨준다. 나는 이때 const[state,setState] = useState
의 구조를 구조 분해 할당을 통해서 넘겨준 것이라 생각해본 적이 없는데 이렇게 보고나니 useState 함수를 구조 분해 할당을 통해 state와 setter함수를 넘겨주고 있는 것이었다. o_o 신기허네,,,
useEffect(callback,deps);
useEffect는 이렇게 생겼다. 우리가 보통 보는 모습으로는
useEffect(()=>{},[]);
이렇게 볼 것이다. 그럼 얘는 어떻게 동작을 할까?
1. Effect 처리: 컴포넌트가 렌더링 된 이후, 부수적인 작업 실행
2. 의존성 배열 관리: 의존성 배열을 통해 특정 값이 변경될 때만 이펙트가 다시 실행되도록 제어
3. 클린업 함수 처리: 이펙트가 다시 실행되기 전에 이전 이펙트의 클린업 작업을 실행할 수 있음
✖ 클린업 함수 : useEffect의 return값! 컴포넌트가 삭제될 경우, 언마운트 되는 경우, useEffect가 다시 실행되기 전에 함수가 실행됨.
그럼 이제 만들어보자
const useEffect = (callback, depArray) => {
const hasNoDeps = !depArray; //의존성 배열 여부
const prevDeps = hooks[currentHook] ? hooks[currentHook].deps : undefined; // 이전 의존성 배열 체크
const prevCleanUp = hooks[currentHook]
? hooks[currentHook].cleanUp
: undefined; // 이전 클린업 함수 체크
const hasChangedDeps = prevDeps
? !depArray.every((el, i) => el === prevDeps[i])
: true; // 의존성 배열이 변경되었는지 확인, 같지않다면 callback을 실행시키고 prevDeps를 유저의 의존성배열로 치환시켜줌
if (hasNoDeps || hasChangedDeps) {
if (prevCleanUp) prevCleanUp(); // 이전 클린업함수가 있다면 그거로 실행
const cleanUp = callback();
hooks[currentHook] = { deps: depArray, cleanUp };
}
currentHook++; // 다음 훅으로 넘어가기
};
이 코드도 주석을 다 써두었으니 하나씩 정리해보자.
hasNoDeps
는 의존성 배열이 존재하는 지 여부이고 prevDeps
는 이전 의존성 배열을 체크하여 존재한다면 hooks배열에서 의존성 배열을 가지고 오고 아니면 undefined로 둔다.
prevCleanUp
또한 이전 클린업 함수의 존재 여부를 찾고 아니면 undefined로 둔다.
hasChagedDeps
는 의존성 배열이 변경되었는지 확인하고 이전과 지금의 의존성 배열을 비교하여 변했다면 callback을 실행시키고 prevDeps를 유저의 의존성 배열로 치환하는 역할을 한다.
조건문은 의존성 배열의 변경 여부와 존재 여부를 따지고 없거나 변경되었따면 prevCleanUp이 존재하면 prevCleanUp을 실행 시키고 콜백을 현재의 클린업으로 할당한 뒤 hooks관리 배열에 넣어준 뒤 관리 배열의 인덱스를 증가시켜준다.
요렇게 만든 Hook들을 아까 만든 MyReact에서 한번 실행시켜보면
import MyReact, { useEffect, useState } from "./MyReact.mjs";
function ExampleComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("foo");
useEffect(() => {
console.log("effect", count, text);
return () => {
console.log("cleanup", count, text);
};
}, [count, text]);
return {
click: () => setCount(count + 1),
type: (text) => setText(text),
noop: () => setCount(count),
render: () => console.log("render", { count, text }), // 현재 상태 출력
};
}
let App = MyReact.render(ExampleComponent); // 초기 렌더링
App.click();
App = MyReact.render(ExampleComponent);
App.type("bar");
App = MyReact.render(ExampleComponent);
App.noop();
App = MyReact.render(ExampleComponent);
App.click();
App = MyReact.render(ExampleComponent);
요렇게 실제로 useState와 useEffect를 사용하는 것 처럼 똑같이 결과를 볼 수 있다.
다른 아티클이나 문서들을 보면 매직이라는 단어를 볼 수 있는데 매직이 이처럼 Hook들이 쉽게 우리가 동작할 수 있도록 해주는 것을 매직이라 하는데 사실 매직이 아니라 이런 동작들로 이루어진 함수에 불과하다 라는 것을 알 수 있다. 이런 함수 뜯어보기 재밌는 것 같다.