inblog logo
|
jay0628
    React

    [React] 10. 블로그 만들기 (4) - 홈

    김주희's avatar
    김주희
    Aug 26, 2025
    [React] 10. 블로그 만들기 (4) - 홈
    Contents
    페이징like 검색전체 순서 (타임라인)기억할 포인트1) 디바운스 적용 (입력 멈춘 뒤 300ms 후 검색)2) 쓰로틀 적용 (입력 중에도 최대 1회/500ms로 검색)
     
    이런 response 안에 data headers 보고 뭐있는지 등 정리하기
    notion image
     
    notion image
     
    // 이 경우에는 list만 있는게 아니므로 object로 관리 const [model, setModel] = useState({ isFirst: , isLast: , list: [], });
     

    key?

    화면에서 위치 등의 변경을 확인하기 위해서 key 필요 (동일한 그림인데 순서가 중요하다면)
    근데 지금 미리 배울 필요없음 (flutter에서도)
    근데 react에서는 key 안하면 컬렉션에서 터져버리기 때문에
     
     
    notion image
     
    아직 빈배열이니까 아무것도 안보임
    notion image
     
    userEffect 함수로 ~할때 처음 통신하고 데이터 받아옴
    notion image
     
    notion image
     
    notion image
     

    페이징

     

    최초의 틀은 만들어두는게 좋다.

    최초에는 전체 페이지를 모르므로 null이 아니라 undefined
     
    근데 안만들어도 돌아감 그냥 {} 형태여도 됨
    notion image
     
    notion image
     
    notion image
     
    dto는 동일하더라도 화면마다 만드는게 좋다.
     
     

    객체의 상태는 행위를 통해 변경한다.

     
    notion image
     
    notion image
     
    클릭하면 model의 상태가 아닌 page의 상태가 변경됨
    notion image
     
    따라서 게시글 내용이 변경되려면 다시 통신해야됨
    즉 내가 원하는 건 page가 변경되었을때 model을 다시 호출하는 것!
     
    어차피 상태이므로 매개변수로 받을 필요 없음
    notion image
    useEffect가 재작동 하도록 변경해야 됨
     
     
     
    1. 페이지 상태 초기화
    1. model 초기화
    1. useEffect가 최초에 실행됨
    1. apiHome이 통신으로 호출됨
    1. 비동기 함수이므로 axios 호출하고 기다리지 않고 빠져나감
    1. prev함수 읽고 next 함수 읽고 실행은X 메모리에 올라가기만
    1. model.boards가 아직 통신 안끝나서 0임
    1. 그림 다 그리고
    1. 이벤트 루프로 돌아감
    1. pending 중인거 확인
    1. 통신 끝났나?
    1. 안끝났으면 밖에서 놀다가 다시 와서 확인
    1. 통신 끝나면 상태 갱신
    1. model.boards.map 부분만 갱신 나머지는 동일하니까
    1. next 버튼 누르면
    1. next 실행
    1. page가 1로 변경
    1. page를 화면에 가지고 있는거 없음
    1. observer가 없음 (화면에 바인딩 된 부분 없음)(
    1. 그래서 page에 의해 그림 다시 그려지는 거 없음
    1. useEffect가 page에 의존하고 있으므로 다시 그려짐
    1. 그때 page에 1이 들어감
       

      결국에는 axios를 내가 만들어봐야 느는것..? axios 쓰는거나 nexacro같은 솔루션 쓰는거나 남이 만든거 가져다 쓰는건 똑같음
       
       

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

      like 검색

       
      page는 생략 가능
      모든 쿼리 스트링은 생략 가능해야됨
      default가 뭔지도 알아봐야 됨
       
      notion image
       
      keyword에 값 없어도 오류 X
      notion image
       
      notion image
       
      최초의 keyword는 공백!
      이걸 확인해보고 시작해야됨
       
      notion image
       
       
      keyword 적을때마다 검색하도록 이벤트 따기
       
      지금은 화면 리렌더링이 안돼서 글자 안적힘
      notion image
       
      notion image
       
      notion image
       
      notion image
       
      상태관리 + 바인딩이 되어야 화면이 다시 그려짐
       
      바인딩이 안되면 sub가 안된것
       
      notion image
       
       
      키워드에 의존만하면 이제 다시 통신함
      notion image
       
      notion image
       

      이제 123을 검색하고 싶은데

      지금은 1쓸때 통신하고 2쓸때도 통신하고 3쓸때도 통신해서 3번 통신함
       
      내가 다 쓰기 전에는 안되도록 타이밍을 잡아야? 디바운싱
       
      디바운싱과 쓰로틀링을 적용하는건 숙제로 알아서 해보기
      [JS] 디바운싱(Debouncing)과 쓰로틀링(Throttling)
      어제 너무 유익한 면접을 보고왔다. 면접 후에 더욱 함께하고 싶은 회사가 되었고, 결과에 상관없이 좋은 면접이었다고 생각한다. 👩‍💻면접관님 회원가입 버튼이 있고, 1초에 100번 누르는 유저가 있다고 가정해봅시다.이러한 경우, 짧은 시간동안 100번의 요청이 생길
      [JS] 디바운싱(Debouncing)과 쓰로틀링(Throttling)
      https://velog.io/@imzzuu/JS-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8B%B1Debouncing%EA%B3%BC-%EC%93%B0%EB%A1%9C%ED%8B%80%EB%A7%81Throttling-feat.-%ED%94%84%EC%97%94-%EB%A9%B4%EC%A0%91
      [JS] 디바운싱(Debouncing)과 쓰로틀링(Throttling)
       
      컬렉션ㄴ으로 쌓아뒀다가 동일하네? 한방에 처리
       
       
       
      notion image
       

       
      notion image
       
       
      좋은 질문! 우선 이 코드에서는 UI는 rawKeyword로 즉시 반영되고, 실제 검색(API 호출)은 keyword가 바뀔 때만 일어나요. (keyword 자체가 UI에 바로 쓰이진 않습니다—입력창 value는 rawKeyword입니다.)
      아래는 “처음 렌더 → 타이핑 → 디바운스 → API 호출 → 페이지 이동”까지의 전체 흐름입니다.

      전체 순서 (타임라인)

      1. 초기 마운트
      • page=0, keyword="", rawKeyword=""로 렌더.
      • useEffect([page, keyword])가 실행 → apiHome() 호출 (page=0, keyword="").
      • 응답 오면 setModel(...) → 목록 렌더.
      1. 사용자가 입력을 타이핑
      • onChange → changeValue(e) 호출.
        • const v = e.target.value 추출 (이벤트 풀링 이슈 방지).
        • setRawKeyword(v) → 바로 재렌더되어 입력창 UI가 즉시 바뀜.
        • debouncedSetKeyword(v) → 300ms 타이머 스케줄.
          • 300ms 내에 또 타이핑하면 이전 타이머 취소되고 새로 300ms 대기(디바운스).
      1. 타이핑 멈추고 300ms 경과
      • 디바운스 타이머 만료 → debouncedSetKeyword 실행:
        • setPage(0) (새 검색 시작이므로 페이지 리셋)
        • setKeyword(v) (실제 검색어 확정)
      • 이 두 상태 변경으로 재렌더 발생.
      1. 의존성 효과로 API 호출
      • 방금 바뀐 page(0) 또는 keyword 변화에 의해 useEffect([page, keyword]) 재실행.
      • apiHome() 호출 → axios.get(...page=0&keyword=...).
      • 응답 오면 setModel(...) → 결과 목록 업데이트.
      1. 페이지네이션 클릭
      • Prev/Next 클릭 시 setPage(p±1) → 재렌더.
      • useEffect([page, keyword])가 다시 실행되어 apiHome() 호출.
      • 이때 keyword는 그대로 유지, rawKeyword도 유지. 결과만 새 페이지로 갱신.
      1. 언마운트 시 정리
      • 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

      jay0628

      RSS·Powered by Inblog