오늘은 useState와 useEffect를 간단하게 만들어보는 실습을 진행했다.
클로저에대해 대강 이해는 해도 어떨 때 쓰는건지 잘 와닿지 않았는데 오늘 조금은 더 알게된 거 같다!
클로저
- 함수와 그 함수가 선언될 때 렉시컬 환경의 조합
- 함수가 생성될 당시의 외부 변수 상태를 기억하고, 이를 함수 호출시에도 계속 접근할 수 있게 해줌
function outer() {
let count = 0; // inner함수가 선언될 때의 환경
return function inner() {
count++;
console.log(`Count: ${count}`);
}
}
const counter = outer();
counter(); // Count: 1
counter(); // Count: 2
counter(); // Count: 3
outer()의 실행이 마무리 됐기 때문에 그 내부의 count도 사라질 것이라 생각할 수 있지만, 반환받은 inner()가 선언될 때의 정보에 계속 접근할 수 있으므로 계속해서 count에 어떤 동작을 취할 수 있다.
useState
필요한 동작
const [state, setState] = useState(initialValue);
- 초기화: 컴포넌트가 렌더링될 때 useState는 initialValue로 상태를 초기화
- 상태 저장: React는 이 상태를 컴포넌트 인스턴스와 함께 저장, 이후에 상태가 변경되어도 값을 유지
- 상태 업데이트: setState를 호출하면 React는 컴포넌트를 리렌더링하고 새로운 상태값을 적용
렌더 함수 만들기
const MyReact = {
render(Component) {
const Comp = Component();
Comp.render(); // 컴포넌트의 렌더링 함수 실행
currentHook = 0; // 컴포넌트가 렌더링되면 hook들을 처음부터 다시 실행하므로, 인덱스를 0으로 되돌려둠
return Comp; // 컴포넌트의 렌더링 함수를 실행하고, 컴포넌트를 반환
},
};
export default MyReact;
import MyReact from "./React.mjs";
function ExampleComponent() {
return {
render: () => console.log("render"),
};
}
let App = MyReact.render(ExampleComponent); // 초기 렌더링
useState 만들기
let _val;
const useState = (initialValue) => {
if (!_val) {
_val = initialValue;
}
function setState(newVal) {
_val = newVal;
}
return [_val, setState];
};
위와 같이 만드는 경우 useState를 여러번 사용했을 때 모든 값이 _val에 저장되기 때문에 문제가 된다.
따라서 hook들이 사용될 때 값을 관리해줄 공간이 필요하다. (배열)
export const useState = (initialValue) => {
// hooks에 값이 있을 때는 그 값을, 없는 경우 initialValue를 hooks[currentHook]에 저장
// 컴포넌트에서 useState를 이용해 처음 state를 선언했을 때가 아직 hooks[currentHook]에 저장된 값이 없을 때!
hooks[currentHook] = hooks[currentHook] || initialValue;
// 나중에 setState가 사용될 때를 위해 선언된 순서를 저장해둠
// 위에서 currentHook을 사용하는 건 const [~,~] = useState(0); 했을 때는 순서대로 실행되니까 currentHook 바로 사용하면 되고
// setState는 순서와 상관없이 컴포넌트 내부에서 언제 호출될지 모르니까 선언된 순서인 currentHook을 저장해두고 hookIndex를 사용??
// 클로저가 있어서 setState가 선언될 때 hookIndex를 기억해둘 수 있다
const hookIndex = currentHook;
const setState = (newState) => {
if (typeof newState === "function") {
// 함수형변환 한 경우 전달받은 newState는 함수일 것이므로 그 함수를 활용해 새로운 값 할당
hooks[hookIndex] = newState(hooks[hookIndex]);
} else {
hooks[hookIndex] = newState;
}
};
// 나중에 선언해둔 state, setState에 접근할 수 있도록 반환
// 다음에 호출할 hook의 호출 순서를 관리하기 위해 currentHook을 1증가
return [hooks[currentHook++], setState];
};
useEffect
필요한 동작
useEffect(callback, deps);
- Effect 처리: 컴포넌트가 렌더링된 이후, 부수적인 작업을 실행
- 의존성 배열 관리: 의존성 배열을 통해 특정 값이 변경될 때만 이펙트가 다시 실행되도록 제어
- 클린업 함수 처리: 이펙트가 다시 실행되기 전에 이전 이펙트의 클린업 작업을 실행할 수 있음
useEffect 만들기
const useEffect = (callback, depArray) => {
// 의존성배열이 있는지 확인
const hasNoDeps = !depArray;
// 이전 Effect가 실행됐을 때의 의존성 배열/cleanUp 을 확인, 첫 실행이라면 undefined
const prevDeps = hooks[currentHook] ? hooks[currentHook].deps : undefined;
const prevCleanUp = hooks[currentHook]
? hooks[currentHook].cleanUp
: undefined;
// 의존성배열에 변화가 있는지 확인
// 전달받은 depArray를 순회하며 이전 실행 시점의 deps와 비교해 모두 같은 값을 가지고있는지 확인
// every()로 모든 요소가 같은지 확인, 하나라도 다르면 !false === true
// 값 자체로 비교하기 때문에 비교할 요소가 참조타입인 경우 [...state]와 같이 새로운 주소에 할당해줘야 하는 것!
const hasChangedDeps = prevDeps
? !depArray.every((el, i) => el === prevDeps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
if (prevCleanUp) prevCleanUp(); // cleanUp이 있는 경우 이전 이펙트의 cleanUp을 먼저 실행
// 전달받은 callback, depArray를 업데이트 해줌
const cleanUp = callback();
hooks[currentHook] = { deps: depArray, cleanUp };
}
currentHook++;
};
전체 코드
// MyReact.js
let hooks = []; // hook을 사용할 때 필요한 여러 값들을 저장해둘 배열
let currentHook = 0; // 현재 실행되고있는 hook의 순서, 이 값을 인덱스로 활용해 위 배열에서 값을 가져오거나 관리한다
const useState = (initialValue) => {
// hooks에 값이 있을 때는 그 값을, 없는 경우 initialValue를 hooks[currentHook]에 저장
// 컴포넌트에서 useState를 이용해 처음 state를 선언했을 때가 아직 hooks[currentHook]에 저장된 값이 없을 때!
hooks[currentHook] = hooks[currentHook] || initialValue;
// 나중에 setState가 사용될 때를 위해 선언된 순서를 저장해둠
// 위에서 currentHook을 사용하는 건 const [~,~] = useState(0); 했을 때는 순서대로 실행되니까 currentHook 바로 사용하면 되고
// setState는 순서와 상관없이 컴포넌트 내부에서 언제 호출될지 모르니까 선언된 순서인 currentHook을 저장해두고 hookIndex를 사용??
// 클로저가 있어서 setState가 선언될 때 hookIndex를 기억해둘 수 있다
const hookIndex = currentHook;
const setState = (newState) => {
if (typeof newState === "function") {
// 함수형변환 한 경우 전달받은 newState는 함수일 것이므로 그 함수를 활용해 새로운 값 할당
hooks[hookIndex] = newState(hooks[hookIndex]);
} else {
hooks[hookIndex] = newState;
}
};
// 나중에 선언해둔 state, setState에 접근할 수 있도록 반환
// 다음에 호출할 hook의 호출 순서를 관리하기 위해 currentHook을 1증가
return [hooks[currentHook++], setState];
};
const useEffect = (callback, depArray) => {
// 의존성배열이 있는지 확인
const hasNoDeps = !depArray;
// 이전 Effect가 실행됐을 때의 의존성 배열/cleanUp 을 확인, 첫 실행이라면 undefined
const prevDeps = hooks[currentHook] ? hooks[currentHook].deps : undefined;
const prevCleanUp = hooks[currentHook]
? hooks[currentHook].cleanUp
: undefined;
// 의존성배열에 변화가 있는지 확인
// 전달받은 depArray를 순회하며 이전 실행 시점의 deps와 비교해 모두 같은 값을 가지고있는지 확인
// every()로 모든 요소가 같은지 확인, 하나라도 다르면 !false === true
// 값 자체로 비교하기 때문에 비교할 요소가 참조타입인 경우 [...state]와 같이 새로운 주소에 할당해줘야 하는 것!
const hasChangedDeps = prevDeps
? !depArray.every((el, i) => el === prevDeps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
if (prevCleanUp) prevCleanUp(); // cleanUp이 있는 경우 이전 이펙트의 cleanUp을 먼저 실행
// 전달받은 callback, depArray를 업데이트 해줌
const cleanUp = callback();
hooks[currentHook] = { deps: depArray, cleanUp };
}
currentHook++;
};
const MyReact = {
render(Component) {
const Comp = Component();
Comp.render(); // 컴포넌트의 렌더링 함수 실행
currentHook = 0; // 컴포넌트가 렌더링되면 hook들을 처음부터 다시 실행하므로, 인덱스를 0으로 되돌려둠
return Comp; // 컴포넌트의 렌더링 함수를 실행하고, 컴포넌트를 반환
},
};
MyReact.useState = useState;
MyReact.useEffect = useEffect;
export { useState, useEffect };
export default MyReact;
// index.js
import MyReact, { useState, useEffect } from "./MyReact.js";
function ExampleComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("foo");
useEffect(() => {
// callback
console.log("effect", count, text);
return (
// callback의 cleanUp
() => {
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);
'챌린지반' 카테고리의 다른 글
[React] useReducer, context API로 투두리스트 만들기 (0) | 2024.08.26 |
---|---|
[React] You might not need an effect를 읽고 프로젝트 effect 수정하기 (0) | 2024.08.20 |
[React] CRA, Vite 없이 개발환경 구축하기 (0) | 2024.08.19 |
[React] 리액트 훅, 리액트를 함수로 분석하기 (0) | 2024.08.14 |