-
Notifications
You must be signed in to change notification settings - Fork 8
[8주차/엘리] 워크북 제출합니다. #80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 엘리/main
Are you sure you want to change the base?
The head ref may contain hidden characters: "\uC5D8\uB9AC/main"
Changes from all commits
29e0288
eb858f2
09143cf
99f6c21
403f78c
86bed0c
6b400ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # **`Debounce`** 개념 정리 | ||
|
|
||
| `Debounce`는 이벤트가 연속적으로 발생할 때, | ||
|
|
||
| **마지막 이벤트가 발생한 뒤 일정 시간이 지나면 딱 한 번만 실행**되도록 만드는 기법이다. | ||
|
|
||
| 예를 들어 사용자가 검색창에 글자를 입력할 때마다 API 요청을 보내면: | ||
|
|
||
| - `r` 입력 → 요청 | ||
| - `re` 입력 → 요청 | ||
| - `rea` 입력 → 요청 | ||
| - ... | ||
|
|
||
| 처럼 너무 많은 요청이 발생한다. | ||
|
|
||
| 이때 `Debounce`를 적용하면 사용자가 입력을 멈춘 뒤 일정 시간(예: 300ms)이 지나고 나서 한 번만 요청을 보낸다. | ||
|
|
||
| 즉, **불필요한 함수 실행을 줄여 성능을 최적화**하는 방식이다. | ||
|
|
||
| ### 동작 방식 | ||
|
|
||
| 1. 이벤트 발생 | ||
| 2. 타이머 시작 | ||
| 3. 지정 시간 안에 이벤트가 또 발생하면 기존 타이머 취소 | ||
| 4. 다시 타이머 시작 | ||
| 5. 마지막 이벤트 이후 일정 시간이 지나면 함수 실행 | ||
|
|
||
| ### 주 사용 사례 | ||
|
|
||
| - 검색 자동완성 | ||
| - 입력값 검증 | ||
| - 자동 저장(auto-save) | ||
| - resize 이벤트 최적화 | ||
|
|
||
| ### Throttle과의 차이 | ||
|
|
||
| | 구분 | Debounce | Throttle | | ||
| | --- | --- | --- | | ||
| | 실행 시점 | 이벤트가 멈춘 뒤 실행 | 일정 시간마다 실행 | | ||
| | 목적 | 마지막 결과만 처리 | 중간 과정도 주기적으로 처리 | | ||
| | 사용 예시 | 검색창 입력 | 스크롤 이벤트 | | ||
|
|
||
| Debounce는 | ||
|
|
||
| “사용자가 행동을 끝냈을 때 실행”에 가깝고, | ||
|
|
||
| Throttle은 | ||
|
|
||
| “계속 발생하더라도 일정 주기로 실행”에 가깝다. | ||
|
|
||
| # **`Debounce`** 코드 작성 | ||
|
|
||
| ```jsx | ||
| function debounce(func, delay) { | ||
| let timeoutId; | ||
|
|
||
| return function (...args) { | ||
| // 기존 타이머 제거 | ||
| clearTimeout(timeoutId); | ||
|
|
||
| // 새로운 타이머 등록 | ||
| timeoutId = setTimeout(() => { | ||
| func.apply(this, args); | ||
| }, delay); | ||
| }; | ||
| } | ||
| ``` | ||
|
|
||
| ### 사용 예시 | ||
|
|
||
| ```jsx | ||
| constsearch=debounce((keyword) => { | ||
| console.log("검색 요청:",keyword); | ||
| },300); | ||
|
|
||
| input.addEventListener("input", (e) => { | ||
| search(e.target.value); | ||
| }); | ||
| ``` | ||
|
|
||
| ### 코드 흐름 설명 | ||
|
|
||
| ```jsx | ||
| clearTimeout(timeoutId); | ||
| ``` | ||
|
|
||
| 기존에 예약된 실행이 있다면 취소한다. | ||
|
|
||
| ```jsx | ||
| timeoutId=setTimeout(...) | ||
| ``` | ||
|
|
||
| 새로운 실행 예약을 만든다. | ||
|
|
||
| 즉, 이벤트가 계속 발생하면 이전 예약은 계속 취소되고 | ||
|
|
||
| 마지막 이벤트 이후 `delay` 시간이 지나야 함수가 실행된다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| # **`Throttling`** 개념 정리 🍠 | ||
|
|
||
| ## `Throttling` 개념 정리 | ||
|
|
||
| `Throttle`은 이벤트가 연속적으로 발생하더라도 | ||
|
|
||
| **일정 시간 간격마다 한 번씩만 함수가 실행되도록 제한하는 기법**이다. | ||
|
|
||
| 예를 들어 스크롤 이벤트는 사용자가 화면을 움직이는 동안 수십~수백 번 발생한다. | ||
|
|
||
| 이때 이벤트마다 함수를 실행하면: | ||
|
|
||
| - 성능 저하 | ||
| - 불필요한 연산 증가 | ||
| - 렌더링 부담 | ||
|
|
||
| 문제가 생길 수 있다. | ||
|
|
||
| `Throttle`을 적용하면 지정한 시간 간격(예: 300ms)마다 한 번씩만 실행되므로 | ||
|
|
||
| 과도한 함수 호출을 줄일 수 있다. | ||
|
|
||
| ### 동작 방식 | ||
|
|
||
| 1. 이벤트 발생 | ||
| 2. 함수 실행 | ||
| 3. 일정 시간 동안 추가 호출 무시 | ||
| 4. 시간이 지나면 다시 실행 가능 | ||
|
|
||
| 즉, | ||
|
|
||
| - 이벤트는 계속 발생하지만 | ||
| - 함수는 정해진 주기마다 실행된다. | ||
|
|
||
| ### 주 사용 사례 | ||
|
|
||
| - scroll 이벤트 | ||
| - resize 이벤트 | ||
| - 마우스 이동(mousemove) | ||
| - 버튼 연속 클릭 방지 | ||
| # **`Throttling`** 코드 작성 🍠 | ||
|
|
||
| ```jsx | ||
| function throttle(func, delay) { | ||
| let lastCall = 0; | ||
|
|
||
| return function (...args) { | ||
| const now = Date.now(); | ||
|
|
||
| // 마지막 실행 이후 delay가 지났는지 확인 | ||
| if (now - lastCall >= delay) { | ||
| lastCall = now; | ||
| func.apply(this, args); | ||
| } | ||
| }; | ||
| } | ||
| ``` | ||
|
|
||
| ### 사용 예시 | ||
|
|
||
| ```jsx | ||
| consthandleScroll=throttle(() => { | ||
| console.log("스크롤 이벤트 실행"); | ||
| },300); | ||
|
|
||
| window.addEventListener("scroll",handleScroll); | ||
| ``` | ||
|
|
||
| ### 코드 흐름 설명 | ||
|
|
||
| ```jsx | ||
| constnow=Date.now(); | ||
| ``` | ||
|
|
||
| 현재 시간을 저장한다. | ||
|
|
||
| ```jsx | ||
| if (now-lastCall>=delay) | ||
| ``` | ||
|
|
||
| 마지막 실행 시점으로부터 `delay` 이상 지났는지 확인한다. | ||
|
|
||
| ```jsx | ||
| lastCall=now; | ||
| ``` | ||
|
|
||
| 함수가 실행되면 마지막 실행 시간을 갱신한다. | ||
|
|
||
| 즉, 이벤트가 아무리 많이 발생해도 | ||
| 설정한 시간 간격마다 함수가 한 번씩만 실행된다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { useEffect, useState } from 'react' | ||
|
|
||
| export function useDebounce<T>(value: T, delay: number): T { | ||
| const [debouncedValue, setDebouncedValue] = useState<T>(value) | ||
|
|
||
| useEffect(() => { | ||
| const timer = setTimeout(() => { | ||
| setDebouncedValue(value) | ||
| }, delay) | ||
|
|
||
| return () => { | ||
| clearTimeout(timer) | ||
| } | ||
| }, [value, delay]) | ||
|
|
||
| return debouncedValue | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { useEffect, useState } from 'react' | ||
|
|
||
| const LG = 1024 // Tailwind lg 브레이크포인트 | ||
|
|
||
| export function useSidebar() { | ||
| // 데스크탑이면 기본 열림, 모바일이면 기본 닫힘 | ||
| const [isOpen, setIsOpen] = useState(() => window.innerWidth >= LG) | ||
|
|
||
| const open = () => setIsOpen(true) | ||
| const close = () => setIsOpen(false) | ||
| const toggle = () => setIsOpen((prev) => !prev) | ||
|
|
||
| // 화면이 모바일 너비로 바뀌면 자동으로 닫기 | ||
| useEffect(() => { | ||
| const mq = window.matchMedia(`(max-width: ${LG - 1}px)`) | ||
| const handleChange = (e: MediaQueryListEvent) => { | ||
| if (e.matches) setIsOpen(false) | ||
| } | ||
| mq.addEventListener('change', handleChange) | ||
| return () => mq.removeEventListener('change', handleChange) | ||
| }, []) | ||
|
|
||
| // ESC 키로 닫기 | ||
| useEffect(() => { | ||
| const handleKeyDown = (e: KeyboardEvent) => { | ||
| if (e.key === 'Escape') setIsOpen(false) | ||
| } | ||
| document.addEventListener('keydown', handleKeyDown) | ||
| return () => document.removeEventListener('keydown', handleKeyDown) | ||
| }, []) | ||
|
|
||
| return { isOpen, open, close, toggle } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { useCallback, useEffect, useRef } from 'react' | ||
|
|
||
| export function useThrottle<Args extends unknown[]>( | ||
| fn: (...args: Args) => void, | ||
| interval: number, | ||
| ): (...args: Args) => void { | ||
| const lastCalledRef = useRef<number>(0) | ||
| const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null) | ||
| // 항상 최신 fn을 참조하되, useCallback 의존성에는 포함하지 않음 | ||
| const fnRef = useRef(fn) | ||
| fnRef.current = fn | ||
|
|
||
| // interval 변경 또는 언마운트 시 대기 중인 trailing 타이머 정리 | ||
| useEffect(() => { | ||
| return () => { | ||
| if (timerRef.current !== null) { | ||
| clearTimeout(timerRef.current) | ||
| timerRef.current = null | ||
| } | ||
| } | ||
| }, [interval]) | ||
|
|
||
| return useCallback( | ||
| (...args: Args) => { | ||
| const now = Date.now() | ||
| const remaining = interval - (now - lastCalledRef.current) | ||
|
|
||
| if (remaining <= 0) { | ||
| // 쿨다운 경과 → 즉시 실행 (leading) | ||
| if (timerRef.current !== null) { | ||
| clearTimeout(timerRef.current) | ||
| timerRef.current = null | ||
| } | ||
| lastCalledRef.current = now | ||
| fnRef.current(...args) | ||
| } else { | ||
| // 쿨다운 중 → 마지막 호출을 trailing으로 예약 | ||
| if (timerRef.current !== null) { | ||
| clearTimeout(timerRef.current) | ||
| } | ||
| timerRef.current = setTimeout(() => { | ||
| lastCalledRef.current = Date.now() | ||
| timerRef.current = null | ||
| fnRef.current(...args) | ||
| }, remaining) | ||
|
Comment on lines
+36
to
+45
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 엘리 코드를 정말 잘 짜셨구 고민을 많이 하신 게 보입니닷 ㅎㅎ 한 가지 추가하자면 지금 이 코드에서 호출이 유입되는 빈도가 interval 경계선 근처에서 요동치게 되면, 기존 타이머를 지우고 새 타이머를 세팅하는 과정에서 remaining 계산의 기준점이 되는 이 부분을 수정해 보면 좋을 것 같아요! |
||
| } | ||
| }, | ||
| [interval], | ||
| ) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 단순히 false로 고정했는데, matchMedia로 화면 크기 변화까지 감지해서 자동으로 닫히도록 한 부분이 반응형 UX를 꼼꼼하게 챙긴 것 같아서 너무 좋은 것 같아요 ! !