이런 response 안에 data headers 보고 뭐있는지 등 정리하기


// 이 경우에는 list만 있는게 아니므로 object로 관리
const [model, setModel] = useState({
isFirst: ,
isLast: ,
list: [],
});key?
화면에서 위치 등의 변경을 확인하기 위해서 key 필요 (동일한 그림인데 순서가 중요하다면)
근데 지금 미리 배울 필요없음 (flutter에서도)
근데 react에서는 key 안하면 컬렉션에서 터져버리기 때문에

아직 빈배열이니까 아무것도 안보임

userEffect 함수로 ~할때 처음 통신하고 데이터 받아옴



페이징
최초의 틀은 만들어두는게 좋다.
최초에는 전체 페이지를 모르므로 null이 아니라 undefined
근데 안만들어도 돌아감 그냥 {} 형태여도 됨



dto는 동일하더라도 화면마다 만드는게 좋다.
객체의 상태는 행위를 통해 변경한다.


클릭하면 model의 상태가 아닌 page의 상태가 변경됨

따라서 게시글 내용이 변경되려면 다시 통신해야됨
즉 내가 원하는 건 page가 변경되었을때 model을 다시 호출하는 것!
어차피 상태이므로 매개변수로 받을 필요 없음

useEffect가 재작동 하도록 변경해야 됨
- 페이지 상태 초기화
- model 초기화
- useEffect가 최초에 실행됨
- apiHome이 통신으로 호출됨
- 비동기 함수이므로 axios 호출하고 기다리지 않고 빠져나감
- prev함수 읽고 next 함수 읽고 실행은X 메모리에 올라가기만
- model.boards가 아직 통신 안끝나서 0임
- 그림 다 그리고
- 이벤트 루프로 돌아감
- pending 중인거 확인
- 통신 끝났나?
- 안끝났으면 밖에서 놀다가 다시 와서 확인
- 통신 끝나면 상태 갱신
- model.boards.map 부분만 갱신 나머지는 동일하니까
- next 버튼 누르면
- next 실행
- page가 1로 변경
- page를 화면에 가지고 있는거 없음
- observer가 없음 (화면에 바인딩 된 부분 없음)(
- 그래서 page에 의해 그림 다시 그려지는 거 없음
- useEffect가 page에 의존하고 있으므로 다시 그려짐
- 그때 page에 1이 들어감
결국에는 axios를 내가 만들어봐야 느는것..? axios 쓰는거나 nexacro같은 솔루션 쓰는거나 남이 만든거 가져다 쓰는건 똑같음

버튼 더 뒤로 안넘어가게 해야됨

like 검색
page는 생략 가능
모든 쿼리 스트링은 생략 가능해야됨
default가 뭔지도 알아봐야 됨

keyword에 값 없어도 오류 X


최초의 keyword는 공백!
이걸 확인해보고 시작해야됨

keyword 적을때마다 검색하도록 이벤트 따기
지금은 화면 리렌더링이 안돼서 글자 안적힘




상태관리 + 바인딩이 되어야 화면이 다시 그려짐
바인딩이 안되면 sub가 안된것

키워드에 의존만하면 이제 다시 통신함


이제 123을 검색하고 싶은데
지금은 1쓸때 통신하고 2쓸때도 통신하고 3쓸때도 통신해서 3번 통신함
내가 다 쓰기 전에는 안되도록 타이밍을 잡아야? 디바운싱
디바운싱과 쓰로틀링을 적용하는건 숙제로 알아서 해보기
컬렉션ㄴ으로 쌓아뒀다가 동일하네? 한방에 처리


좋은 질문! 우선 이 코드에서는 UI는
rawKeyword로 즉시 반영되고, 실제 검색(API 호출)은 keyword가 바뀔 때만 일어나요. (keyword 자체가 UI에 바로 쓰이진 않습니다—입력창 value는 rawKeyword입니다.)아래는 “처음 렌더 → 타이핑 → 디바운스 → API 호출 → 페이지 이동”까지의 전체 흐름입니다.
전체 순서 (타임라인)
- 초기 마운트
page=0,keyword="",rawKeyword=""로 렌더.
useEffect([page, keyword])가 실행 →apiHome()호출 (page=0, keyword="").
- 응답 오면
setModel(...)→ 목록 렌더.
- 사용자가 입력을 타이핑
onChange→changeValue(e)호출.const v = e.target.value추출 (이벤트 풀링 이슈 방지).setRawKeyword(v)→ 바로 재렌더되어 입력창 UI가 즉시 바뀜.debouncedSetKeyword(v)→ 300ms 타이머 스케줄.- 300ms 내에 또 타이핑하면 이전 타이머 취소되고 새로 300ms 대기(디바운스).
- 타이핑 멈추고 300ms 경과
- 디바운스 타이머 만료 →
debouncedSetKeyword실행: setPage(0)(새 검색 시작이므로 페이지 리셋)setKeyword(v)(실제 검색어 확정)
- 이 두 상태 변경으로 재렌더 발생.
- 의존성 효과로 API 호출
- 방금 바뀐
page(0) 또는keyword변화에 의해useEffect([page, keyword])재실행.
apiHome()호출 →axios.get(...page=0&keyword=...).
- 응답 오면
setModel(...)→ 결과 목록 업데이트.
- 페이지네이션 클릭
- Prev/Next 클릭 시
setPage(p±1)→ 재렌더.
useEffect([page, keyword])가 다시 실행되어apiHome()호출.
- 이때
keyword는 그대로 유지,rawKeyword도 유지. 결과만 새 페이지로 갱신.
- 언마운트 시 정리
useEffect의 cleanup에서debouncedSetKeyword.cancel()호출 → 남아있던 디바운스 예약 취소(언마운트 후 setState 방지).
기억할 포인트
- UI 즉시 반영: 입력창은
rawKeyword에 바인딩되어 즉시 반영됨.
- 검색 트리거:
keyword는 디바운스로 늦게 바뀌며, 이 변경이 API 호출을 유발(useEffect([page, keyword])).
- 검색 시작 시 페이지 리셋: 디바운스 콜백에서
setPage(0)→ 새 검색은 항상 0페이지부터.
- 빠른 타이핑: 300ms 안의 변화는 합쳐져서 한 번만 호출(서버 부하 감소).
- 경계 사례: 사용자가 다른 페이지를 보고 있는 중에 타이핑을 멈추면, 300ms 뒤에
setPage(0)이 실행되어 다시 0페이지로 검색됨(의도대로 동작).
- 책
좋아요! 지금 코드에 lodash로 디바운스/쓰로틀을 각각 적용하는 방법을 딱 맞게 붙여볼게요. (핵심: 입력 값은 즉시 화면에 반영하고, 실제 검색에 쓰이는 keyword는 지연/제한해서 변경)
1) 디바운스 적용 (입력 멈춘 뒤 300ms 후 검색)
- 사용자가 타이핑을 멈춘 뒤에만 API 호출되게 하는 방식
- 입력 지연 처리 전용
rawKeyword(입력창 값) 상태와 실제 검색에 쓰이는keyword상태를 분리
import React, { useEffect, useMemo, useState } from "react";
import { Form, FormControl, Pagination } from "react-bootstrap";
import BoardItem from "../../components/BoardItem";
import axios from "axios";
import { debounce } from "lodash";
const Home = () => {
const [page, setPage] = useState(0);
// 입력창에 즉시 반영되는 값
const [rawKeyword, setRawKeyword] = useState("");
// 실제로 검색(API 호출)에 사용하는 값
const [keyword, setKeyword] = useState("");
const [model, setModel] = useState({
totalPage: undefined,
number: 0,
isFirst: true,
isLast: false,
boards: [],
});
// ✅ 디바운스된 setter (300ms)
const debouncedSetKeyword = useMemo(
() =>
debounce((value) => {
// 새 검색 시작 시 페이지를 0으로 리셋
setPage(0);
setKeyword(value);
}, 300),
[]
);
useEffect(() => {
// 컴포넌트 unmount 시 디바운스 큐 정리
return () => debouncedSetKeyword.cancel();
}, [debouncedSetKeyword]);
useEffect(() => {
apiHome();
}, [page, keyword]);
async function apiHome() {
const response = await axios.get(
`http://localhost:8080?page=${page}&keyword=${encodeURIComponent(keyword)}`
);
setModel(response.data.body);
}
function prev() {
setPage((p) => p - 1);
}
function next() {
setPage((p) => p + 1);
}
function changeValue(e) {
const v = e.target.value; // ⚠️ React 이벤트 풀링 주의: 먼저 값 꺼내기
setRawKeyword(v); // UI에는 즉시 반영
debouncedSetKeyword(v); // 검색은 300ms 디바운스
}
return (
<div>
<Form className="d-flex mb-4" onSubmit={(e) => e.preventDefault()}>
<FormControltype="search"
placeholder="Search"
className="me-2"
aria-label="Search"
value={rawKeyword}
onChange={changeValue}
/>
</Form>
{model.boards.map((board) => (
<BoardItem key={board.id} id={board.id} title={board.title} page={0} />
))}
<br />
<div className="d-flex justify-content-center">
<Pagination>
<Pagination.Item onClick={prev} disabled={model.isFirst}>
Prev
</Pagination.Item>
<Pagination.Item onClick={next} disabled={model.isLast}>
Next
</Pagination.Item>
</Pagination>
</div>
</div>
);
};
export default Home;
2) 쓰로틀 적용 (입력 중에도 최대 1회/500ms로 검색)
- 사용자가 계속 타이핑하더라도 최대 500ms에 한 번만 검색 상태가 바뀌도록 제한
- 마지막 입력은
trailing: true로 보장되게 설정
import React, { useEffect, useMemo, useState } from "react";
import { Form, FormControl, Pagination } from "react-bootstrap";
import BoardItem from "../../components/BoardItem";
import axios from "axios";
import { throttle } from "lodash";
const Home = () => {
const [page, setPage] = useState(0);
const [rawKeyword, setRawKeyword] = useState("");
const [keyword, setKeyword] = useState("");
const [model, setModel] = useState({
totalPage: undefined,
number: 0,
isFirst: true,
isLast: false,
boards: [],
});
// ✅ 쓰로틀된 setter (500ms, 마지막 호출 보장)
const throttledSetKeyword = useMemo(
() =>
throttle(
(value) => {
setPage(0);
setKeyword(value);
},
500,
{ leading: true, trailing: true }
),
[]
);
useEffect(() => {
return () => throttledSetKeyword.cancel();
}, [throttledSetKeyword]);
useEffect(() => {
apiHome();
}, [page, keyword]);
async function apiHome() {
const response = await axios.get(
`http://localhost:8080?page=${page}&keyword=${encodeURIComponent(keyword)}`
);
setModel(response.data.body);
}
function prev() {
setPage((p) => p - 1);
}
function next() {
setPage((p) => p + 1);
}
function changeValue(e) {
const v = e.target.value;
setRawKeyword(v); // 입력창은 즉시 업데이트
throttledSetKeyword(v); // 검색은 500ms 쓰로틀
}
return (
<div>
<Form className="d-flex mb-4" onSubmit={(e) => e.preventDefault()}>
<FormControltype="search"
placeholder="Search"
className="me-2"
aria-label="Search"
value={rawKeyword}
onChange={changeValue}
/>
</Form>
{model.boards.map((board) => (
<BoardItem key={board.id} id={board.id} title={board.title} page={0} />
))}
<br />
<div className="d-flex justify-content-center">
<Pagination>
<Pagination.Item onClick={prev} disabled={model.isFirst}>
Prev
</Pagination.Item>
<Pagination.Item onClick={next} disabled={model.isLast}>
Next
</Pagination.Item>
</Pagination>
</div>
</div>
);
};
export default Home;
어떤 걸 쓰면 좋을까?
- 검색창: 보통 디바운스(사용자 타이핑 멈추면 호출) → 서버 부하 최소화 & 사용감 좋음
- 스크롤/리사이즈 이벤트: 쓰로틀(주기적으로 제한) → 프레임 드랍 방지
설치:
npm i lodash
# 또는 트리쉐이킹 좋아하는 경우
npm i lodash-es
// lodash-es 사용 시
import { debounce, throttle } from "lodash-es";
Share article
![[JS] 디바운싱(Debouncing)과 쓰로틀링(Throttling)](https://image.inblog.dev?url=https%3A%2F%2Fvelog.velcdn.com%2Fimages%2Fimzzuu%2Fpost%2Fdf471d3e-2802-4853-ad51-3aa01c59600b%2Fimage.png&w=1920&q=75)