예전 회사에서 react 뉴비일 때(지금도 뉴비지만…) 아래와 같은 코드를 짰었다. fetch를 수행하는 getArticlesPromise 함수를 호출해 값을 받아오고 이를 useState로 업데이트 치기. 그리고 articles를 화면에 그린다. But 흐름상 자연스레 setArticles 가 호출되어 articles 값이 업데이트 되어 화면에 뿌려줄 줄 알았는데 안 된다.
const [articles, setArticles] = useState();
const getArticlesPromise= () => {
axios.post('https://example.com/article', params, config)
.then(function (response) {
if( response.data) return response.data;
else return null;
})
.catch(function (error) {
console.log(error);
});
}
useEffect(() => {
const fetchArticles = () => {
const articleData = getArticlesPromise();
setArticles(articleData);
};
fetchArticles();
});
return (
<div>
<ul>
{articles}
</ul>
</div>
);
비동기 문제인가 해서 타임아웃을 걸어봐도 안 되고 async, await를 써 봐도 안 된다. getArticlesPromise 가 값을 못 가져오는 것인가? 하면 그렇지 않다. 잘 가져온다.
그러다 아래와 같이 방법을 찾았다. getArticlesPromise에서 값을 fetch 하는게 아니라 호출 함수만 리턴하고 then으로 chain을 연결해 가져오면 된다. 뭔가 뚜렷하진 않지만 되긴 한다. 당시에도 then을 연결해 promise를 다뤄야 한다고 기록해 놨었다.
const [articles, setArticles] = useState();
const getArticlesPromise = async () => {
return await axios.post('https://example.com/article', params, config)
};
useEffect(() => {
const fetchArticles = () => {
const articleData = getArticlesPromise();
articleData.then(function(res) {
if( res.data ) setArticles(res.data);
else console.log('error');
})
.catch(function (error) {
console.log(error);
});
}
fetchArticles();
},[]);
return (
<div>
<ul>
{articles}
</ul>
</div>
);
시간이 많이 지나 회사에서 react state가 바로 업데이트 안 되는 원인이 이슈가 되었고 리액트 공홈에서 아래와 같은 글을 읽게 되었다.
https://beta.reactjs.org/learn/state-as-a-snapshot
https://beta.reactjs.org/learn/queueing-a-series-of-state-updates
영어라 여러번 읽어야 이해가 되었다. 결론은 react에서 state 업데이트가 안 되게 막았다는 것.
아래는 공홈 링크에 있는 예제이다.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
버튼을 한 번 클릭하면 number값이 3이 될 것 같지만 결과는 1이다. 클릭할 때마다 숫자는 1씩 올라간다. 왜 그럴까? 왜 state 값이 클릭마다 바로 업데이트 되지 않고 한 번씩만 수행될까?
링크의 설명을 빌리자면 렌더링은 react가 컴포넌트를 호출하는 것인데 컴포넌트 안에는 props, 이벤트 핸들러, 변수등이 있고 이를 계산해서 스냅샷을 찍어 던져준다고 한다. 그럼 react는 스냅샷을 받아 화면에 그리는 렌더링을 하게 된다.
위에 예제에서 setNumber는 세 번 호출되었지만 react는 한 번의 렌더링, 하나의 스냅샷에 state 값을 한 번만 바꾼다. 다음 state 값 변경은 다음 렌더링에서 한 번만 허용한다. 그러니 세 번 호출해도 한 번만 호출한 것으로 처리한다. 다음 렌더링에도 state 변경은 한 번만 허용하기에 아무리 많이 클릭해도 값은 1씩 올라간다.
개발자 입장에서는 의아하다. 왜 이렇게 만들었을까? 그러나 사용자 입장에서 생각해보면 이해가 된다.
아래와 같은 예제가 있다고 하자.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
}}>+1</button>
</>
)
}
사용자가 버튼을 클릭했다. number 값은 1이 될 것이다. 렌더링을 위해 스냅샷을 찍고 사용자에게 던져주는 찰나의 순간에 사용자가 클릭을 100번 했다고 치자. 그럼 던져주는 와중의 스냅샷 값을 취소하고 다시 number가 100으로 바뀐 스냅샷을 던져줘야 하나? 아니면 스냅샷을 중간에 가로채서 100으로 업데이트 치고 던져줘야 하나? 스냅샷은 그대로 유지한다고 해도 클릭 한 번에 1 이었던 값이 다음 클릭 후 렌더링때는 101을 리턴한다면 그것도 일관성에는 문제가 있어 보인다.
react에서 스냅샷 하나에 state 값을 한 번만 변경하게 만든 이유는 그게 일관성을 유지할 수 있는 가장 깔끔한 방법이기 때문이다. 그 당시의 상태를 찍어서 던져주는 스냅샷 구조에서 state 변경을 계속 허용하면 일관성이 깨져 버린다.
위의 공홈 링크에서 바로 업데이트 되게 하는 방법도 제시하는데 아래와 같이 함수로 등록하면 queue에 넣은 후 전부 실행해 준다고 한다.
setNumber(n => n + 1);
내가 최초에 짰던 fetch 함수의 문제를 해결했던 방법도 then 안에서 함수로 호출하면서 react queue에 함수가 등록되었고 이게 순차적으로 실행되면서 결과값을 바로 볼 수 있었던 것이다. 그야말로 소 뒷걸음 치다가 쥐 잡은 격.
결론: useState 에서 state 값은 한 번만 바뀐다. 스냅샷의 일관성 유지를 위해 react가 그렇게 만들어졌다. 이를 피하고 싶으면 state 업데이트를 함수로 실행시키면 된다.
링크에 있는 예제이다.