React에서 무한 스크롤 기능을 구현중이다. 키워드를 검색하면 Naver Open API에서 관련된 제목을 가진 영화들을 배열로 받아와 list로 10개씩 보여주고, 끝까지 스크롤되면 다음 data 10개를 받아와서 list에 추가하는 기능이다.
아래의 코드는 프로젝트의 무한스크롤 기능 부분이다.
// App.js
const [items, setItems] = useState(null);
const [display, setDisplay] = useState(10);
const [query, setQuery] = useState("");
...
const getData = () => {
// `/v1/search/movie.json?query=${query}&display=${display}`으로
GET요청을 보내어 받아온 data배열을 items에 setState함,
query에 관련된 data를 display만큼 받아옴
...
};
useEffect(() => {
const scrollHandler = () => {
// 스크롤을 문서의 끝까지 내리면 현재 items갯수 + 10의 값을 setDisplay한 뒤 getData한다.
if (getScrollTop() >= getDocumentHeight() - window.innerHeight) {
const nextDisplay = items.length + 10;
// 문제의 부분
setDisplay(nextDisplay);
getData();
}
};
window.addEventListener("scroll", scrollHandler);
return () => {
window.removeEventListener("scroll", scrollHandler);
};
}, [items, getData, display]);
문제는 scrollHandler의 setDisplay부분이다. 처음 스크롤이 끝까지 닿았을 땐 setDisplay가 작동을 안 하다가, 두 번째로 스크롤이 끝까지 닿으면 그 때 setDisplay가 작동한다.
나는 이게 왜 안 되는지 아주 답답했지만, 해결하고 난 뒤에 다시 보면 내가 문제일 것임을 알기 때문에 코드를 짜낸 나를 탓하며 디버깅을 한다...
setDisplay만 이렇게 작동하는가 싶어 test State를 만들어 실험해보았다.
// App.js
cosnt [test, setTest] = useState(0);
...
useEffect(() => {
const scrollHandler = () => {
if (getScrollTop() >= getDocumentHeight() - window.innerHeight) {
const nextDisplay = items.length + 10;
setDisplay(nextDisplay);
console.log(test);
setTest(17);
console.log(test);
getData();
}
};
결과는 test State도 똑같이 동작했다. 그렇다면 scrollHanlder 내부에서 setState는 제대로 동작하지 않는다 가 다음으로 의심해볼만한 가설이다.
구글링을 해보았지만 scrollHanlder 안에서 setState가 동작하지 않는다는 이슈는 없었다. 그리고 나의 경우 아예 동작하지 않는 것도 아니고 처음엔 동작 안 하다가 두 번째에 동작하고 있다.
그러다가 해결에 대한 실마리를 잡았다.
이슈에 대해 구글링을 하며 다른 무한 스크롤 코드를 보니 getData 함수 내부에 setDisplay를 두는 것이다. 나도 따라해봤다.
// App.js
const getData = useCallback(
async (e) => {
if (e) {
e.preventDefault();
}
if (query === "") {
alert("검색어를 입력해주십시오.");
return;
}
try {
setLoading(true);
const {
data: { items: data },
} = await getMovieList(query, display);
setItems(data);
// 데이터를 fetch해서 items에 넣은 다음 items.length + 10을 setDisplay한다.
setDisplay(items.length + 10);
setLoading(false);
} catch (e) {
console.log(e);
setLoading(false);
}
},
[query, display, items]
);
...
하지만 결과는 똑같았다.
근데 items.length가 아니라 그냥 상수를 줬더니 생각대로 잘 동작하는 것이다. scrollHandler 내부에서 상수를 setState 했을 때에는 결과가 그대로였었다.
// App.js
const getData = useCallback(
async (e) => {
...
try {
setLoading(true);
const {
data: { items: data },
} = await getMovieList(query, display);
setItems(data);
// 그냥 상수 20으로 setDisplay한다.
setDisplay(20);
setLoading(false);
} catch (e) {
console.log(e);
setLoading(false);
}
},
[query, display, items]
);
...
보다시피 내가 처음 생각했던 대로 잘 된다. 근데 이게 동작하는 순서가 내 생각과 다르다. 데이터를 읽어오자마자 display state가 업데이트된다.
getData()
가 호출되고 데이터를 읽어온 뒤 setDisplay(20)
이 호출된다.getData()
가 호출되고, 이전에 20으로 업데이트 된 display로 데이터를 읽어온다.??? 그럼 위에서 내가 구현했던 코드는 뭔가?
getData()
가 호출되고 데이터를 읽어온다.setDisplay(20)
이 호출되고, getData()
가 호출되어 업데이트 된 display로 데이터를 읽어온다.setDisplay()
가 호출되는 시점이 다르다. 그래도 여전히 이해가 안 된다. 데이터를 읽어오기 이전에 display를 업데이트한다는 부분은 매한가지이지 않은가
더 놀라운 것은 getData()
안에서 setDisplay(display + 10)
는 정상적으로 작동한다는 것이다
// App.js
const getData = useCallback(
async (e) => {
...
try {
setLoading(true);
const {
data: { items: data },
} = await getMovieList(query, display);
setItems(data);
// 이전 display 값에 + 10을 setDisplay 한다.
setDisplay(display + 10);
setLoading(false);
} catch (e) {
console.log(e);
setLoading(false);
}
},
[query, display, items]
);
...
해결은 했으나 이유를 모르니 미칠 노릇이다. 두고두고 생각해 볼 문제이다.