순수함수는 2가지 특징을 가진다.
- 입력이 같으면 결과도 같다.
- 부작용이 없다.
몇번을 실행하더라도 자신의 일만 할 뿐, 함수 바깥 공간을 건드려서는 안된다.
컴포넌트의 render 함수는 순수해야한다. 즉 , 컴포넌트의 state를 변경하지 않고 호출될 때마다 동일한 결과를 반환해야하고 브라우저와 직접적으로 상호작용하지 않는다.
const Counter = () => {
const [count, setCount] = React.useState(0)
document.title = `count: ${count}`
const handleClick = () => setCount(count + 1)
return <button onClick={handleClick}>plus</button>
}
export default Counter
1) document api를 직접 함수 컴포넌트 내에서 호출하고 있다. 이는 동기적으로 동작함으로 좀 더 복잡한 로직이었다면 반응속도를 늦출 수 있다.
2) 전역변수를 건드림으로 순수 함수 처럼 동작하지 않는다.
function useEffect(effect) {
function runDeferedEffect() {
const ENOUGH_TIME_TO_RENDER = 1;
setTimeout(effect, ENOUGH_TIME_TO_RENDER);
}
runDeferedEffect()
}
return { useState, useEffect};
useEffect를 통해 렌더링 성능에 영향을 주는 api 호출을 렌더링이 다 끝난 후에 실행하도록 할 수 있다.
const Counter = () => {
const [count, setCount] = React.useState(0);
MyReact.useEffect(() => {
document.title = `count: ${count}`;
console.log("effect1");
});
const handleClick = () => setCount(count + 1);
console.log("Counter rendered");
return <button onClick={handleClick}>plus</button>
};
실행 시킬 함수를 전달하면 의도한대로 동작한다.
const Counter = () => {
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState("");
const handleClick = () => setCount(count + 1);
const handleChangeName = (e) => {
setName(e.target.value)
};
MyReact.useEffect(() => {
document.title = `count: ${count}`;
console.log("effect1");
}, count);
console.log("Counter rendered");
return (
<>
<button onClick={handleClick}>plus</button>
<input value={name} onChange={handleChangeName}></input>
</>
);
};
하지만 여러개의 state가 있을 때 매번 useEffect가 불필요하게 실행되는것도 문제가 된다.
위 예시에서는 state name이 하나 더 추가 되었다. handleChangeName으로 인해 re render 될때마다 useEffect도 같이 호출이 되어버린다. 이를 방지하고 싶다.
function useEffect(effect, nextDep) {
function runDeferedEffect() {
const ENOUGH_TIME_TO_RENDER = 1;
setTimeout(effect, ENOUGH_TIME_TO_RENDER);
}
if (!isInitialized[cursor]) {
isInitialized[cursor] = true;
dep = nextDep;
runDeferedEffect();
console.log("init")
return;
}
console.log("already init cursor = ", cursor)
if (dep === nextDep) return;
dep = nextDep;
runDeferedEffect();
}
return { useState, useEffect };
})();
아직 초기화 되지않은 제일 첫 rendering이면 useEffect를 호출하게 되지만 이후 useEffect 인자로 넘겨준 값이 변화가 없으면 넘겨 받은 callback 함수를 실행하지 않고 진행한다.
이번에 여러 useEffect를 한 component에 쓰기를 원하고 이후에 component unmount가 되면 사용했던 부수효과들이 초기화 되기를 원한다.
function useEffect(effect, nextDeps) {
function runDeferedEffect() {
function runEffect() {
const cleanup = effect();
if (cleanup) cleanups[cursor] = cleanup
}
const ENOUGH_TIME_TO_RENDER = 1;
setTimeout(runEffect, ENOUGH_TIME_TO_RENDER);
}
if (!isInitialized[cursor]) {
isInitialized[cursor] = true;
deps[cursor] = nextDeps;
cursor += 1;
runDeferedEffect();
return;
}
const prevDeps = deps[cursor];
const depsSame = prevDeps.every((prevDep, index) => prevDep === nextDeps[index])
if (depsSame) {
cursor += 1;
return;
}
deps[cursor] = nextDeps;
cursor += 1;
runDeferedEffect();
}
function resetCursor() {
cursor = 0;
}
function cleanupEffects() {
cleanups.forEach(cleanup => typeof cleanup === "function" && cleanup())
}
return { useState, useEffect, resetCursor, cleanupEffects };
})();
nextDeps에서는 배열로서 변화가 일어날 variable들의 배열을 주입받고
depsSame에서 배열을 돌며 그 이전값과 변동이 있을 경우에만 주입받은 effect (callback)을 실행하고
unmount시에 client쪽에서 cleanupEffects를 호출할 수 있게 return 해준다.
const Counter = () => {
MyReact.resetCursor();
const [count, setCount] = React.useState(0);
const [name, setName] = React.useState("");
const handleClick = () => setCount(count + 1);
const handleChangeName = (e) => {
setName(e.target.value);
};
MyReact.useEffect(() => {
document.title = `count: ${count} | name: ${name}`;
console.log("effect1");
return function cleanup() {
document.title = "";
console.log("effect1 cleanup");
};
}, [count, name]);
MyReact.useEffect(() => {
localStorage.setItem("name", name);
console.log("effect2");
}, [name]);
MyReact.useEffect(() => {
setName(localStorage.getItem("name") || "");
}, []);
console.log("Counter rendered");
return (
<>
<button onClick={handleClick}>plus</button>
<input value={name} onChange={handleChangeName}></input>
</>
);
};
export default () => {
const [mounted, setMounted] = React.useState(false);
const handleToggle = () => {
const nextMounted = !mounted;
if (!nextMounted) MyReact.cleanupEffects();
setMounted(nextMounted);
};
return (
<>
<button onClick={handleToggle}>component toggle</button>
{mounted && <Counter />}
</>
);
};
unmount를 재현하기 위해서 Counter component를 별도의 클래스로 감싸서 unmount시에 cleanupEffects를 호출했다.
clean up을 해주니 url이 변경되는것을 알 수 있다. 실제 사용시에는 counter component가 DomTree에 들어오기전에 title 제목을 어딘가 저장해놓고 꺼내서 unmount시 되돌려 놓는 방식으로 사용한다.
클래스 컴포넌트는 메소드 별로 각자의 역할을 수행한다. render 메소드는 리액트 앨리먼트를 처리하고 componentDidMount 같은 생명주기 메서드는 부수효과를 처리한다. 컴포넌트의 생명 주기에 맞춰 컴포넌트의 동작을 기술하는 것이 클래스 컴포넌트의 방식이다.
함수 컴포넌트는 리액트 엘리먼트만 처리한다. useEffect는 함수 컴포넌트가 할 수없는 부수효과를 실행하는 역할을 맡는다.
부수효과는 컴포넌트의 생명주기와는 관련이 없다. 함수 컴포넌트는 생명주기가 없기 때문이다. 단지 의존성으로 전달한 상태와 인자 값에 따라 부수효과가 동작한다.
리액트는 컴포넌트를 표현하는 상태와 인자가 변할 때 부수효과를 실행한다. 컴포넌트의 상태 (인자를 포함) 와 외부 환경(부수효과) 동기화 시키는 셈이다.
- 컴포넌트 상태 count - 브라우져 제목 간 동기화 (document.title 맞추기 [외부환경])
- 컴포넌트 상태 name - 로컬 스토리지 간 동기화
- ProductPage 컴포넌트의 상태 product - 데이터베이스 간 동기화 (아직 안나옴)
'FrontEnd > [리액트 2부] 고급 주제와 훅' 카테고리의 다른 글
[4.1장 레프 훅][4.2장 레프 훅] (0) | 2024.03.19 |
---|---|
[3.5장 컨텍스트 훅] , custom hook (0) | 2024.03.16 |
[3.1장 클래스/함수 컴포넌트][3.2장 상태훅] (0) | 2024.03.11 |
[2.4장 다이얼로그1][2.5장 다이얼로그2] (0) | 2024.03.07 |
[2.2장 라우터 1] [2.3장 라우터 2] (0) | 2024.03.04 |