useEffect(()=>{
console.log('useEffect..');
}, []);
useEffect 훅의 부수효과함수에서 API 를 호출하는 경우
export default User({userId}) {
const [user, setUser] = useState(null);
useEffect(()=>{
getUser(userId).then(result => setUser(result.data.user));
});
}
getUser() 함수를 호출하여 사용자를 조회해온다는 API 가 있다고 가정을 해보자.
User 컴포넌트가 렌더링을 할때마다 호출이 되면 비효율적이다. 이 문제를 해결하기 위해 의존성 배열에 빈 배열을 넣을 수도 있다. 하지만, userId 가 변경되도 새로운 사용자를 조회해오지 않는다면 올바른 해결책이 아니다.
export default User({userId}) {
const [user, setUser] = useState(null);
useEffect(()=>{
getUser(userId).then(result => setUser(result.data.user));
},[userId]);
}
userId 가 변경이 되었을때만, 부수효과함수를 실행하도록 해결한 코드다.
나중에 부수효과 함수를 수정할 때는 새로 추가된 변수를 빠짐없이 의존성 배열에 추가해야 한다.
export default User({userId, needDetail}) {
const [user, setUser] = useState(null);
useEffect(()=>{
getUser(userId, needDetail).then(result => setUser(result.data.user));
},[userId]);
}
needDetail 이라는 속성값을 부수효과 함수에서 사용했다. 새로운 상태값 또는 속성값을 사용했다면 의존성 배열에 추가해야한다. (사용자 조회 시, needDetail 이 바뀌었는데 조회를 해오지 않는다면 문제다)
그런데, 이게 값이 많아지면 깜빡하는경우도 발생한다.
-> 이를 해결하기 위해 eslint 에서 사용할 수 있는 exhaustive-deps 규칙을 사용하면, 잘못 사용된 의존성 배열을 찾아서 알려준다.
export default function MyComponent() {
const [value1, setValue1] = useState(0);
const [value2, setValue2] = useState(0);
useEffect(()=>{
const interval = setInterval(() => {
console.log(value1, value2);
},1000);
return () => {
clearInterval(interval);
}
},[value1]);
return (
<div>
<button onClick={()=> setValue1(value1+1)}>value1 증가</button>
<button onClick={()=> setValue2(value2+1)>value2 증가</button>
</div>
)
}
value2 는 의존성 배열에 넣지 않았다. value2 값을 증가시켜도 부수효과 함수는 갱신되지 않으며, value2 가 변경되기 전에 등록된 부수효과 함수가 계속 사용된다.
즉, 부수효과함수가 생성된 시점에 value2 를 참조하므로 value2 를 증가시켜도 초기값 0을 참조하는 것이다.
useEffect 훅에서 async ~ await 함수 사용하기
const [user, setUser] = useState(null);
useEffect(async ()=>{
const userData = await getUser();
setUser(userData);
},[]);
promise 객체를 반환하므로 부수효과 함수가 될 수 없음.
useEffect 훅에서 async ~ await 함수를 사용하는 방법은 부수효과함수내에서 async ~ await 함수를 만들어서 호출하는거다.
const [user, setUser] = useState(null);
useEffect(()=>{
async function getUserData() {
const userData = await getUser();
setUser(userData);
}
getUserData();
},[]);
😲 useEffect 훅에서 getUserData() 함수를 재사용해야한다면 어떻게 해야할까? 간단하다. useEffect 훅 밖으로 빼주면 된다.
const [user, setUser] = useState(null);
useEffect(()=>{
getUserData();
},[getUserData]);
async function getUserData() {
const userData = await getUser();
setUser(userData);
}
useEffect 훅에서 getUserData() 함수를 사용하므로, 해당 함수를 의존성 배열에 추가해준다.
그런데 컴포넌트가 렌더링될때마다 getUserData() 함수는 갱신되므로 결과적으로 useEffect 훅은 렌더링될때마다 부수효과함수를 실행한다. 이 문제를 해결하려면 getUserData() 함수가 필요할 때만 갱신되도록 만들어야 한다. useCallback 훅을 이용해보자.
const [user, setUser] = useState(null);
useEffect(()=>{
getUserData();
},[getUserData]);
const getUserData = useCallback(async ()=>{
const userData = await getUser();
setUser(userData);
},[]);
😌 useCallback 훅을 이용해서 불필요한 함수생성을 막았다.
의존성 배열을 없애는 방법
export default function User({userId}) {
const [user, setUser] = useState(null);
useEffect(()=>{
if(!userId && !user && user.id !== userId) {
getUser(userId)
}
});
}
😌 조건문으로 getUser() 함수 호출시점을 관리한다.
의존성 배열을 입력하지 않으면, 부수효과함수에서 사용된 모든 변수는 가장 최신화된 값을 참조하므로 안심할 수 있다.
useState 훅의 상태값 변경함수에 함수로 인자를 전달하거나, useReducer 훅 사용, useRef 훅을 사용하여 의존성 배열을 사용하지 않게 개선이 가능하다.
React.memo() 함수로 렌더링 결과 재사용하기
function User({name}) {
return (
<div>
<p>{name}</p>
</div>
)
}
export default React.memo(User)
// 비교함수를 직접 작성도 가능하다.
export default React.memo(
User,
(prevProps, nextProps) => prevProps.name === nextProps.name
);
😌 이제 name 속성값이 변경된 경우에만 렌더링 된다.
React.memo() 함수의 두번째 인자인 속성값 비교함수를 입력하지 않으면 리액트에서 기본으로 제공하는 함수를 사용한다.
prevProps.prop1 === nextProps.prop1 && prevProps.prop2 === nextProps.prop2 && ...
속성값과 상태값을 불변 변수로 관리하기
function Child({onClick}) {
return (
<div>
<button onClick={onClick}>클릭 이벤트</button>
</div>
)
}
export default Parent(){
const [count, setCount] = useState(0);
function clickEvent() {
//...
}
return (
<div>
<p>{count}</p>
<button onClick={()=>setCount(count+1)}>숫자값 증가</button>
<Child onClick={()=>clickEvent}/>
</div>
)
}
Parent 컴포넌트의 속성값이 변경되면 렌더링이 발생을 하는데, 이때 clickEvent() 함수를 생성해서 자식 컴포넌트에게 전달을 해주는데 자식컴포넌트는 늘 속성값이 변경되었구나라고 인식한다.(실제로는 변경된 함수가 아님)
😈 React.memo() 함수로 Child 컴포넌트를 감싸서 생성하면 해결이 가능할까? 라는 생각도 들지만 onClick 속성값이 늘 새로운 함수로 들어가기때문에 소용이 없다.
useState, useReducer 훅의 상태값 변경 함수는 변하지 않는다는 점을 이용하면 이 문제를 쉽게 해결이 가능.
만약, 상태값 변경외에 다른 처리도 필요하다면 useCallback 훅을 사용할 수 있다.
function Child({onClick}) {
return (
<div>
<button onClick={onClick}>클릭 이벤트</button>
</div>
)
}
export default Parent(){
const [count, setCount] = useState(0);
const clickEvent = useCallback(() => {
//...
},[]);
return (
<div>
<p>{count}</p>
<button onClick={()=>setCount(count+1)}>숫자값 증가</button>
<Child onClick={clickEvent}/>
</div>
)
}
useCallback 훅을 이용해서 이벤트 처리 함수를 구현했으며, 의존성 배열로 빈 배열을 입력했으므로 이 함수는 항상 고정된 값을 가짐.
객체의 값이 변하지 않도록 관리
- 함수와 마찬가지로 컴포넌트 내부에서 객체를 정의해서 자식 컴포넌트 속성값으로 전다하면, 객체의 내용이 변경되지 않았는데도 속성값이 변경됐다고 인식한다.
function SelectItem({items, onChange}) {
// ...
}
export default function SelectResult() {
return (
<div>
<SelectItem items={[{name : 'item1', price : 100}, {name : 'item2', price : 200}]}
onChange={//...}
/>
</div>
)
}
SelectResult 컴포넌트가 렌더링될때마다 items 속성값에 새로운 참조값을 생성해서 전달한다.
items 속성값은 항상 같은 값을 가지므로 다음과 같이 컴포넌트 외부에서 상수로 관리할 수 있따.
function SelectItem({items, onChange}) {
// ...
}
export default function SelectResult() {
return (
<div>
<SelectItem items={ITEMS}
onChange={//...}
/>
</div>
)
}
const ITEMS = [{name : 'item1', price : 100}, {name : 'item2', price : 200}]
이제 items 속성값은 항상 같은값이 입력된다. 그런데 이번에는 컴포넌트 내부에서 계산되는 연산이 있는데, 이걸 최소환으로 실행하려면 useMemo 훅을 이용한다.
function SelectItem({items, onChange}) {
// ...
}
export default function SelectResult() {
const limitedPriceItems = useMemo(()=> ITEMS.filter(item => item.price <= 200), []);
return (
<div>
<SelectItem items={ITEMS}
onChange={//...}
/>
</div>
)
}
const ITEMS = [{name : 'item1', price : 100}, {name : 'item2', price : 200}]
* 무조건 useMemo, useCallback, React.memo() 등을 사용하는 것은 바람직하지 않다. 성능 이슈가 발생했을 때 해당하는 부분의 코드만 최적화 하도록 하자!
가상 DOM 에서의 최적화
export default App() {
const [flag, setFlag] = useState(false);
useEffect(() => {
setTimeout(()=> {
setFlag(true);
}, 2000)
}, [])
return (
if(flag) {
return (
<div>
<Child/>
<p>메롱메롱<p/>
</div>
)
} else {
<span>
<Child/>
<p>메롱메롱2<p/>
</span>
}
)
}
2초 후에 div 요소로 변경된다. 이때, 부모요소가 변경되면서 자식컴포넌트 및 자식요소들이 삭제 후에 다시 추가된다.
export default App() {
const [flag, setFlag] = useState(true);
useEffect(() => {
setTimeout(()=> {
setFlag(false);
}, 2000)
}, [])
return (
if(flag) {
return (
<div>
<Child/>
<p>메롱메롱1<p/>
</div>
)
} else {
<div>
<Child/>
<p>메롱메롱1<p/>
<p>메롱메롱2<p/>
</div>
}
)
}
2초 후에 메롱메롱2 라는 요소를 추가한다. 가상 DOM 비교를 통해 앞의 두 요소가 변경되지 않았다는 것을 알며, 실제 DOM 에는 메롱메롱2만 추가한다.
그런데, 메롱메롱3 를 중간에 넣으면 어떻게 될까??
export default App() {
const [flag, setFlag] = useState(true);
useEffect(() => {
setTimeout(()=> {
setFlag(false);
}, 2000)
}, [])
return (
if(flag) {
return (
<div>
<Child/>
<p>메롱메롱1<p/>
<p>메롱메롱2<p/>
<p>메롱메롱3<p/>
</div>
)
} else {
<div>
<Child/>
<p>메롱메롱1<p/>
<p>메롱메롱2<p/>
<p>메롱메롱4<p/>
<p>메롱메롱3<p/>
</div>
}
)
}
중간에 요소를 추가하면 그 뒤에 있는 요소가 변경되지 않았다는 것을 알지 못함!!
메롱메롱3 가 변경되지 않았다는 것을 알기 위해서는 모든 값을 비교해야 하므로 연산량은 기하급수적으로 늘어난다. 리액트는 효율적으로 연산하기 위해 순서 정보를 이용한다.
이러한 문제는 key 속성값을 이용하면 해결이 가능하다. key 속성값을 입력하면 리액트는 같은 키를 가지는 요소끼리만 비교한다.
export default App() {
const [flag, setFlag] = useState(true);
useEffect(() => {
setTimeout(()=> {
setFlag(false);
}, 2000)
}, [])
return (
if(flag) {
return (
<div>
<Child key='child'/>
<p key='메롱메롱1'>메롱메롱1<p/>
<p key='메롱메롱2'>메롱메롱2<p/>
<p key='메롱메롱3'>메롱메롱3<p/>
</div>
)
} else {
<div>
<Child key='child'/>
<p key='메롱메롱1'>메롱메롱1<p/>
<p key='메롱메롱2'>메롱메롱2<p/>
<p key='메롱메롱4'>메롱메롱4<p/>
<p key='메롱메롱3'>메롱메롱3<p/>
</div>
}
)
}