● 리액트에게 의존성으로 거짓말하는게 좋을까? ([ ], 빈배열로 한번만 실행하는게 조을까?)
function Greeting({ name }) {
const [counter, setCounter] = useState(0);
useEffect(() => {
document.title = 'Hello, ' + name;
}, [name]);
...
}
위의 예제에서 props에서 받아오는 name이 변하게 된다면 변할 때마다 렌더링 될 것입니다.
만약 'Dan'이였다가 'Yuzhi'로 변하게 된다면, 아래와 같은 절차를 밟아 확인할 것입니다.
의존성이 변했으니 렌더링이 되겠죠? 하지만 빈배열을 넣는다면 어떻게 될까요?
의존성이 같으므로 이펙트를 스킵할 것입니다.
그렇다면, 1초마다 숫자가 올라가는 카운터를 만들어본다고 가정해봅시다.
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
};
export default App;
우리의 예상 : 마운트될 때 setInterval 함수가 1번 실행되기때문에 계속해서 count가 오를 것이다. 그리고 언마운트될때 이 setInterval함수가 지워질 것이다. 그래서 시계처럼 1초씩 오를 것이다..
하지만.. 계속해서 1만 출력되는 걸 볼 수 있습니다.
우리는 count를 쓰면서 쓰지 않는다고 빈배열을 넣어 거짓말을 했습니다. 이런 것때문에 버그가 터지는 것은 시간 문제 입니다..!!
심지어 권장치 않습니다.
그럼 딱 한번만 실행되고 setInterval 함수가 실행되지 않는 건가?
그게 아니라, 계속 실행되고 있지만, 처음 마운트 되었던 setInterval 함수는 count 가 0일 때를 보고 있는 것이기 때문에 계속해서 setCount (0 + 1) 를 실행하고 있는 것입니다.
// 첫 번째 렌더링
const App = () => {
const [count, setCount] = useState(0);
useEffect(()=>{
const id = setInterval(() => {
setCount(count + 1); // 0 + 1을 호출한다.
}, 1000);
return () => clearInterval(id);
}, []);
...
}
// 매번 렌더링마다 state는 1이다.
const App = () => {
const [count, setCount] = useState(0);
useEffect(()=>{
// 아래에 있는 effect는 무시된다. 빈배열이기 때문에
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
...
}
● 의존성을 솔직하게 적는 방법
그렇다면, 의존성을 솔직하게 적어보도록 해봅시댜.. 의존성 배열에 count를 넣어서 실행하면 원하는 대로 보여지게 됩니다.
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
return(
<h3>count : {count}</h3>
)
};
하지만 위의 예제를 보면 렌더링 될때마다 setInterval함수를 설정하고 해제시킵니다. 이는 매우 효율적이지 못합니다.
그렇다면 어떻게 해야할까요?
우리는 의존성 배열에 count 를 넘겨주고 있습니다. useEffect 내부에서 count를 사용하고 있기 때문이죠. 꼭 count 를 받아야할까요?
- 우리는 마운트 되었을 때, 카운터를 딱 한번만 실행하고 싶고
- 의존성 배열에는 [ ] 빈배열을 넣고싶습니다.
const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(()=>{
setCount(c=> c + 1)
}, 1000)
}, []);
return(
<h3>count : {count}</h3>
)
};
우리가 리액트에게 알려줘야 하는 것은 지금 값이 뭐든 간에 상태 값을 하나 더하라는 것입니다.
따라서 setCount 함수 안에 함수 형태의 업데이터를 만들게 된다면 계속해서 + 1을 해줄 것입니다.
즉, 이펙트 함수 내에서 더이상 count를 불러오지 않습니다.
자.. 그렇다면 count 수를 1이 아니라 사용자가 입력한 수로 지속해서 증가하게 끔 하려면 어떻게 해야할까요?
const App = () => {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(()=>{
setCount(c=> c+ step)
}, 1000)
return ()=> clearInterval(id)
}, [step]);
return(
<div>
<h3>count : {count}</h3>
<input value={step} onChange={(e)=>{setStep(Number(e.target.value))}}/>
</div>
)
};
step state를 또 만들고, 의존성 배열에 step을 넣어주었습니다. step이 바뀔 때마다 setInterval 이 제거되고, 바뀐 step state에 맞게 interval 함수가 다시 설정이 됩니다.
그런데, 우리는 여기서 setInterval 함수를 다시 만들지 않고 싶다면 어떻게 해야할까요? 한번만 마운트되고, 그냥 step값만 바뀌게 하려면? 즉, 의존성 배열을 비어있게하려면?
● 액션을 업데이트로부터 분리시키기
import { useEffect, useReducer } from 'react'
function reducer(state, action){
const {count, step} = state;
switch(action.type){
case 'tick' :
return {count : count + step, step};
case 'step' :
return {count, step : action.step};
default :
throw new Error();
}
}
const Counter = ()=>{
const [state, dispatch] = useReducer(reducer, {count : 0, step: 1})
const {count, step} = state;
useEffect(()=>{
const id = setInterval(()=>{
dispatch({type : 'tick'});
}, 1000)
return ()=> clearTimeout(id);
},[dispatch])
return(
<div>
<h1>{count}</h1>
<input value={step} onChange={(e)=>{dispatch({ type : 'step', step: Number(e.target.value)})}}/>
</div>
)
}
export default Counter;
아마 “이게 뭐가 더 좋아요?” 라고 물어보실 수 있습니다.
리액트는 컴포넌트가 유지되는 한 dispatch 함수가 항상 같다는 것을 보장합니다.
여기서 질문..!!
- 렌더링마다 함수들도 속해있는 곳이 다르니까 함수도 각각 다르다고 인식한다면서요..? dispatch도 함수 아님?
- 함순디 왜 변하지 않았다고 인식함?
dispatch 함수는 useCallback, useMemo를 사용하는 것처럼 렌더링 간의 dispatch 함수 참조가 변경되지는 않습니다. 따라서 의존성 배열에 넣어도 불필요한 렌더링이 되지 않는 것입니다.
useReducer에 의해 반환된 디스패치 함수를 useEffect에 대한 종속성으로 전달하면 렌더링 간에 함수 참조가 변경되지 않는다는 것은 사실입니다. 따라서 React는 디스패치 함수를 종속성으로 사용한다고 해서 불필요하게 효과를 다시 실행하지 않습니다.
사실, React 16.8부터 useEffect 문서에는 디스패치 함수를 종속성으로 포함하는 것이 안전하다고 구체적으로 언급되어 있습니다. - chat GPT-
않이 그럼 근데 그냥 빈배열 넣으면 되는거 아니예여? dispatch 꼭 써야댐?
리액트가 dispatch, setState, useRef 컨테이너 값이 항상 고정되어 있다는 것을 보장하니까 의존성 배열에서 뺄 수도 있습니다. 하지만 명시한다고 해서 나쁠 것은 없습니다.
-리액트 리덕스 만든이.. Dan Abramov-
그렇다고 합니다 ^^;;
일단.. 알겟어요.. 알겠다고.. 근데 그럼 props에서 값 받아올 땐 어떡함? 이럴 때는 무조건 ㅈㅐ렌더링 되는거아님? 놉.
App.js
import { useState } from 'react'
import Counter from './Counter'
const App = ()=>{
const [step, setStep] = useState(1);
return (
<>
<Counter step={step} />
<input value={step} onChange={(e)=>{setStep(Number(e.target.value))}} />
</>
)
}
export default App;
Counter.js
import { useEffect, useReducer } from 'react'
const Counter = ({step})=>{
const [count, dispatch] = useReducer(reducer, 0);
function reducer(state, action){
switch (action.type){
case 'tick' :
return state + step;
default :
throw new Error();
}
}
useEffect(()=>{
const id = setInterval(()=>{
dispatch({type : 'tick'});
}, 1000);
return ()=> clearInterval(id);
},[dispatch]);
return(
<div>
<h1>{count}</h1>
</div>
)
}
export default Counter;
props를 사용하기 위해 reducer함수를 컴포넌트 안쪽에 넣어 정의를 해주었습니다.
dispatch는 렌더링간 동일성을 보장하기 때문에 아까와 같이 똑같이 작동하게 됩니다.
props가 변하더라도 이전 렌더링과 비교해서 의존성 배열에 있는 dispatch는 함수는 같은 참조이기 때문에 한번만 useEffect를 실행하는 것입니다.
않이 눈으로 보여주셈 못믿겟삼
useEffect(()=>{
console.log('실행됨ㅋ')
const id = setInterval(()=>{
dispatch({type : 'tick'});
}, 1000);
return ()=> clearInterval(id);
},[dispatch]);
useEffect안에 실행됨ㅋ 을 넣고 props값을 수정해보았습니다.
아무리 props값을 수정해도 실행됨ㅋ 이 마운트될때만 실행되는 것을 볼 수 있습니다.
저는 useReducer 를 Hooks의 “치트 모드” 라고 생각합니다. 업데이트 로직과 그로 인해 무엇이 일어나는지 서술하는 것을 분리할 수 있도록 만들어줍니다. -댄옹-
따라서 불필요한 렌더링을 제거하기 위해 useReducer를 사용하길 권장한다고 합니다.
● useEffect에 함수가? 비동기 함슈?..
우리는 컴포넌트가 마운트되었을 때, axios를 사용해서 데이터를 가져오고, 그걸 state에 담길 원할 때가 많습니다. 대체로 아래와 같은 상황으로 useEffect함수를 활용해왔을 것입니다.
const App = ()=>{
const [data, setData] = useState();
async function getData(){
const result = await axios.get('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');
setData(result.data);
}
useEffect(()=>{
getData(); // 이거 괜찬??;;
}, [])
....
일단 이 방식은 작동은 합니다. 하지만 여러개 함수들로 나눠 axios요청을 하게 됐다고 가정해봅시다.
const App = ()=>{
const [query, setQuery] = useState('react');
const [data, setData] = useState();
function getFetchUrl(){
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
async function getData(){
const result = await axios.get(getFetchUrl());
setData(result.data);
}
useEffect(()=>{
getData();
}, [])
...
}
export default App;
이 방법도 정상적으로 작동하겟지만, 위의 어떤 함수 중에서 하나라도 state와 props를 사용하게 된다면 ? ? 이펙트는 업데이트된 props, state에 따라 데이터 요청을 할 수가 업겟죠..?
만약 어떠한 함수를 이펙트 안에서만 쓴다면, 그 함수를 직접 이펙트 안으로 옮기세요.
const App = ()=>{
const [data, setData] = useState();
useEffect(()=>{
function getFetchUrl(){
return 'https://hn.algolia.com/api/v1/search?query=react'
}
async function getData(){
const result = await axios.get(getFetchUrl());
setData(result.data);
}
getData();
}, [])
왜 안에 옮겨써야할까요? 이렇게 적게된다면 우리는 더이상 옮겨지는 의존성에 신경 쓸 필요가 업서집니다. 진짜로 이펙트 안에서 컴포넌트의 범위 바깥에 있는 그 어떠한 것도 사용하지 않고 있습니다.!
그렇다면 쿼리 파라미터가 변함에 따라 데이터 요청을 하고 싶다면?
const App = ()=>{
const [query, setQuery] = useState('react');
const [data, setData] = useState();
useEffect(()=>{
function getFetchUrl(){
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
async function getData(){
const result = await axios.get(getFetchUrl());
setData(result.data);
}
getData();
}, [query])
의존성에 query만 추가해주면 됩니다.! !
- 즉 정리하자면, useEffect를 활용해 서버에서 데이터를 가져오는 비동기 처리를 하고싶다면 관련 함수를 모두 useEffect안에 넣어주자.
잠깐만여 저 이거 effect에 다 못넣는데??
function getFetchUrl(query){
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
useEffect(()=>{
const url = getFetchUrl('react');
// ...데이터가지고 뭔가를함
}, [])
useEffect(()=>{
const url = getFetchUrl('redux');
// ...데이터가지고 뭔가를함
}, [])
않이 저는 동시에 두번 받아오고 싶어서 getFetchUrl 함수 useEffect안에 못넣을거같은데요??? 어케함?
근데 이렇게 적게 된다면, getFetchUrl은 각 컴포넌트의 렌더링 마다 고유한 함수를 갖게 될 것입니다. 즉, 렌더링때마다 다 참조가 다른 함수라는거죠. 이거 효율적이지못하네엽..
사실 getFetchUrl에서 변하는건 매개변수 밖에 없는데 말이죵.
해결책 두가지가 있읍니다.
- 아예 함수를 바깥으로 빼버리기 (컴포넌트 밖으로)
function getFetchUrl(query){
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
const App = ()=>{
const [data, setData] = useState();
useEffect(()=>{
const url = getFetchUrl('react');
// ...데이터가지고 뭔가를함
}, [])
useEffect(()=>{
const url = getFetchUrl('redux');
// ...데이터가지고 뭔가를함
}, [])
..
먼저, 함수가 컴포넌트 스코프 안의 어떠한 것도 사용하지 않는다면, 컴포넌트 외부로 끌어올려두고 이펙트 안에서 자유롭게 사용하면 됩니다.
- useCallback으로 감싸기
const getFetchUrl = useCallback((query) => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, []);
useEffect(()=>{
const url = getFetchUrl('react');
// ...데이터가지고 뭔가를함
}, [getFetchUrl])
useEffect(()=>{
const url = getFetchUrl('redux');
// ...데이터가지고 뭔가를함
}, [getFetchUrl])
이렇게 useCallback을 쓰게 된다면 함수자체가 필요할 때만 바뀌게 됩니다.
근데 우리는 state를 받고, state가 변함에 따라 데이터를 받아오고 싶다면?
const [query, setQuery] = useState('react');
const getFetchUrl = useCallback(() => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]);
useEffect(()=>{
const url = getFetchUrl();
// ...데이터가지고 뭔가를함
}, [getFetchUrl])
useCallback 덕분에 query 가 같다면, getFetchUrl 또한 같을 것이며, 이펙트는 다시 실행되지 않을 것입니다. 하지만 만약 query 가 바뀐다면, getFetchUrl 또한 바뀌며, 데이터를 다시 페칭할 것입니다. 마치 엑셀 스프레드시트에서 어떤 셀을 바꾸면 다른 셀이 자동으로 다시 계산되는 것과 비슷합니다.
그럼 query를 props로 받아오고 자식에서 데이터를 사용하고 싶다면?
App.js
const App = ()=>{
const [query, setQuery] = useState('react');
const getFetchUrl = useCallback(() => {
const url = 'https://hn.algolia.com/api/v1/search?query=' + query;
//.. 데이터 불러오는 작업쓰
}, [query]);
return(
<Child getFetchUrl={getFetchUrl}></Child>
)
}
Child.js
const Child = ({ getFetchUrl })=>{
const [data, setData] = useState(null);
useEffect(()=>{
getFetchUrl().then(setData)
}, [getFetchUrl])
}