goodchuck-utils 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/dev/IdSelector.d.ts +41 -0
- package/dist/components/dev/IdSelector.d.ts.map +1 -0
- package/dist/components/dev/IdSelector.js +52 -0
- package/dist/components/dev/index.d.ts +8 -0
- package/dist/components/dev/index.d.ts.map +1 -0
- package/dist/components/dev/index.js +7 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +7 -0
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +11 -0
- package/dist/hooks/useCopyToClipboard.d.ts +67 -0
- package/dist/hooks/useCopyToClipboard.d.ts.map +1 -0
- package/dist/hooks/useCopyToClipboard.js +79 -0
- package/dist/hooks/useDebounce.d.ts +47 -0
- package/dist/hooks/useDebounce.d.ts.map +1 -0
- package/dist/hooks/useDebounce.js +60 -0
- package/dist/hooks/useEventListener.d.ts +79 -0
- package/dist/hooks/useEventListener.d.ts.map +1 -0
- package/dist/hooks/useEventListener.js +33 -0
- package/dist/hooks/useIntersectionObserver.d.ts +109 -0
- package/dist/hooks/useIntersectionObserver.d.ts.map +1 -0
- package/dist/hooks/useIntersectionObserver.js +128 -0
- package/dist/hooks/usePrevious.d.ts +58 -0
- package/dist/hooks/usePrevious.d.ts.map +1 -0
- package/dist/hooks/usePrevious.js +67 -0
- package/dist/hooks/useThrottle.d.ts +57 -0
- package/dist/hooks/useThrottle.d.ts.map +1 -0
- package/dist/hooks/useThrottle.js +80 -0
- package/dist/hooks/useToggle.d.ts +49 -0
- package/dist/hooks/useToggle.d.ts.map +1 -0
- package/dist/hooks/useToggle.js +56 -0
- package/dist/hooks/useWindowSize.d.ts +58 -0
- package/dist/hooks/useWindowSize.d.ts.map +1 -0
- package/dist/hooks/useWindowSize.js +79 -0
- package/package.json +22 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Intersection Observer를 사용하여 요소의 가시성을 감지하는 hook
|
|
4
|
+
*
|
|
5
|
+
* @param ref - 관찰할 요소의 ref
|
|
6
|
+
* @param options - Intersection Observer 옵션
|
|
7
|
+
* @returns IntersectionObserverEntry 또는 undefined
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // 기본 사용 - 요소가 화면에 보이는지 감지
|
|
11
|
+
* function LazyImage({ src }: { src: string }) {
|
|
12
|
+
* const imageRef = useRef<HTMLImageElement>(null);
|
|
13
|
+
* const entry = useIntersectionObserver(imageRef, {
|
|
14
|
+
* threshold: 0.1,
|
|
15
|
+
* freezeOnceVisible: true
|
|
16
|
+
* });
|
|
17
|
+
* const isVisible = entry?.isIntersecting;
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <img
|
|
21
|
+
* ref={imageRef}
|
|
22
|
+
* src={isVisible ? src : undefined}
|
|
23
|
+
* alt="lazy loaded"
|
|
24
|
+
* />
|
|
25
|
+
* );
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // 무한 스크롤
|
|
30
|
+
* function InfiniteScrollList() {
|
|
31
|
+
* const loadMoreRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
* const entry = useIntersectionObserver(loadMoreRef, {
|
|
33
|
+
* threshold: 1.0
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* useEffect(() => {
|
|
37
|
+
* if (entry?.isIntersecting) {
|
|
38
|
+
* loadMoreItems();
|
|
39
|
+
* }
|
|
40
|
+
* }, [entry?.isIntersecting]);
|
|
41
|
+
*
|
|
42
|
+
* return (
|
|
43
|
+
* <div>
|
|
44
|
+
* {items.map(item => <Item key={item.id} {...item} />)}
|
|
45
|
+
* <div ref={loadMoreRef}>Loading...</div>
|
|
46
|
+
* </div>
|
|
47
|
+
* );
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // 애니메이션 트리거
|
|
52
|
+
* function AnimatedSection() {
|
|
53
|
+
* const sectionRef = useRef<HTMLElement>(null);
|
|
54
|
+
* const entry = useIntersectionObserver(sectionRef, {
|
|
55
|
+
* threshold: 0.5,
|
|
56
|
+
* freezeOnceVisible: true
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* return (
|
|
60
|
+
* <section
|
|
61
|
+
* ref={sectionRef}
|
|
62
|
+
* className={entry?.isIntersecting ? 'fade-in' : 'fade-out'}
|
|
63
|
+
* >
|
|
64
|
+
* Content
|
|
65
|
+
* </section>
|
|
66
|
+
* );
|
|
67
|
+
* }
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // rootMargin 사용 (요소가 화면에 들어오기 전에 미리 감지)
|
|
71
|
+
* function PreloadImage({ src }: { src: string }) {
|
|
72
|
+
* const imageRef = useRef<HTMLImageElement>(null);
|
|
73
|
+
* const entry = useIntersectionObserver(imageRef, {
|
|
74
|
+
* rootMargin: '200px', // 화면 기준 200px 전에 감지
|
|
75
|
+
* freezeOnceVisible: true
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* return <img ref={imageRef} src={entry?.isIntersecting ? src : undefined} />;
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
export function useIntersectionObserver(ref, options = {}) {
|
|
82
|
+
const { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false } = options;
|
|
83
|
+
const [entry, setEntry] = useState();
|
|
84
|
+
const frozen = entry?.isIntersecting && freezeOnceVisible;
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const element = ref.current;
|
|
87
|
+
// 요소가 없거나, 이미 frozen 상태면 observer 생성하지 않음
|
|
88
|
+
if (!element || frozen) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Intersection Observer가 지원되지 않는 환경 체크
|
|
92
|
+
if (!window.IntersectionObserver) {
|
|
93
|
+
console.warn('IntersectionObserver is not supported in this browser');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
97
|
+
setEntry(entry);
|
|
98
|
+
}, { threshold, root, rootMargin });
|
|
99
|
+
observer.observe(element);
|
|
100
|
+
return () => {
|
|
101
|
+
observer.disconnect();
|
|
102
|
+
};
|
|
103
|
+
}, [ref, threshold, root, rootMargin, frozen]);
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 요소가 화면에 보이는지 여부만 반환하는 간단한 버전
|
|
108
|
+
*
|
|
109
|
+
* @param ref - 관찰할 요소의 ref
|
|
110
|
+
* @param options - Intersection Observer 옵션
|
|
111
|
+
* @returns 요소가 화면에 보이는지 여부
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* function Section() {
|
|
115
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
116
|
+
* const isVisible = useIsVisible(ref);
|
|
117
|
+
*
|
|
118
|
+
* return (
|
|
119
|
+
* <div ref={ref}>
|
|
120
|
+
* {isVisible ? 'I am visible!' : 'I am hidden'}
|
|
121
|
+
* </div>
|
|
122
|
+
* );
|
|
123
|
+
* }
|
|
124
|
+
*/
|
|
125
|
+
export function useIsVisible(ref, options) {
|
|
126
|
+
const entry = useIntersectionObserver(ref, options);
|
|
127
|
+
return entry?.isIntersecting ?? false;
|
|
128
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 이전 렌더링의 값을 저장하는 hook
|
|
3
|
+
*
|
|
4
|
+
* @param value - 추적할 값
|
|
5
|
+
* @returns 이전 렌더링의 값 (첫 렌더링에서는 undefined)
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // 값 변경 감지
|
|
9
|
+
* function Counter() {
|
|
10
|
+
* const [count, setCount] = useState(0);
|
|
11
|
+
* const prevCount = usePrevious(count);
|
|
12
|
+
*
|
|
13
|
+
* return (
|
|
14
|
+
* <div>
|
|
15
|
+
* <p>Current: {count}</p>
|
|
16
|
+
* <p>Previous: {prevCount}</p>
|
|
17
|
+
* <p>Changed: {count !== prevCount ? 'Yes' : 'No'}</p>
|
|
18
|
+
* <button onClick={() => setCount(count + 1)}>Increment</button>
|
|
19
|
+
* </div>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // 증가/감소 방향 표시
|
|
25
|
+
* function PriceDisplay({ price }: { price: number }) {
|
|
26
|
+
* const prevPrice = usePrevious(price);
|
|
27
|
+
*
|
|
28
|
+
* const trend = prevPrice === undefined
|
|
29
|
+
* ? null
|
|
30
|
+
* : price > prevPrice
|
|
31
|
+
* ? '📈 Up'
|
|
32
|
+
* : price < prevPrice
|
|
33
|
+
* ? '📉 Down'
|
|
34
|
+
* : '➡️ Same';
|
|
35
|
+
*
|
|
36
|
+
* return (
|
|
37
|
+
* <div>
|
|
38
|
+
* <span>${price}</span>
|
|
39
|
+
* {trend && <span>{trend}</span>}
|
|
40
|
+
* </div>
|
|
41
|
+
* );
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // 애니메이션 방향 결정
|
|
46
|
+
* function AnimatedList({ items }: { items: string[] }) {
|
|
47
|
+
* const prevItems = usePrevious(items);
|
|
48
|
+
* const isAdding = prevItems && items.length > prevItems.length;
|
|
49
|
+
*
|
|
50
|
+
* return (
|
|
51
|
+
* <ul className={isAdding ? 'slide-in' : 'slide-out'}>
|
|
52
|
+
* {items.map(item => <li key={item}>{item}</li>)}
|
|
53
|
+
* </ul>
|
|
54
|
+
* );
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
export declare function usePrevious<T>(value: T): T | undefined;
|
|
58
|
+
//# sourceMappingURL=usePrevious.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePrevious.d.ts","sourceRoot":"","sources":["../../src/hooks/usePrevious.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,SAAS,CAWtD"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 이전 렌더링의 값을 저장하는 hook
|
|
4
|
+
*
|
|
5
|
+
* @param value - 추적할 값
|
|
6
|
+
* @returns 이전 렌더링의 값 (첫 렌더링에서는 undefined)
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // 값 변경 감지
|
|
10
|
+
* function Counter() {
|
|
11
|
+
* const [count, setCount] = useState(0);
|
|
12
|
+
* const prevCount = usePrevious(count);
|
|
13
|
+
*
|
|
14
|
+
* return (
|
|
15
|
+
* <div>
|
|
16
|
+
* <p>Current: {count}</p>
|
|
17
|
+
* <p>Previous: {prevCount}</p>
|
|
18
|
+
* <p>Changed: {count !== prevCount ? 'Yes' : 'No'}</p>
|
|
19
|
+
* <button onClick={() => setCount(count + 1)}>Increment</button>
|
|
20
|
+
* </div>
|
|
21
|
+
* );
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // 증가/감소 방향 표시
|
|
26
|
+
* function PriceDisplay({ price }: { price: number }) {
|
|
27
|
+
* const prevPrice = usePrevious(price);
|
|
28
|
+
*
|
|
29
|
+
* const trend = prevPrice === undefined
|
|
30
|
+
* ? null
|
|
31
|
+
* : price > prevPrice
|
|
32
|
+
* ? '📈 Up'
|
|
33
|
+
* : price < prevPrice
|
|
34
|
+
* ? '📉 Down'
|
|
35
|
+
* : '➡️ Same';
|
|
36
|
+
*
|
|
37
|
+
* return (
|
|
38
|
+
* <div>
|
|
39
|
+
* <span>${price}</span>
|
|
40
|
+
* {trend && <span>{trend}</span>}
|
|
41
|
+
* </div>
|
|
42
|
+
* );
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // 애니메이션 방향 결정
|
|
47
|
+
* function AnimatedList({ items }: { items: string[] }) {
|
|
48
|
+
* const prevItems = usePrevious(items);
|
|
49
|
+
* const isAdding = prevItems && items.length > prevItems.length;
|
|
50
|
+
*
|
|
51
|
+
* return (
|
|
52
|
+
* <ul className={isAdding ? 'slide-in' : 'slide-out'}>
|
|
53
|
+
* {items.map(item => <li key={item}>{item}</li>)}
|
|
54
|
+
* </ul>
|
|
55
|
+
* );
|
|
56
|
+
* }
|
|
57
|
+
*/
|
|
58
|
+
export function usePrevious(value) {
|
|
59
|
+
// ref는 리렌더링 간에 값을 유지
|
|
60
|
+
const ref = useRef(undefined);
|
|
61
|
+
// 렌더링 후에 현재 값을 ref에 저장
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
ref.current = value;
|
|
64
|
+
}, [value]);
|
|
65
|
+
// 현재 렌더링에서는 이전 값을 반환
|
|
66
|
+
return ref.current;
|
|
67
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 값의 업데이트를 일정 시간 간격으로 제한하는 hook
|
|
3
|
+
* debounce와 달리 일정 간격마다 최신 값을 업데이트합니다.
|
|
4
|
+
*
|
|
5
|
+
* @param value - throttle할 값
|
|
6
|
+
* @param interval - 업데이트 간격 (밀리초, 기본값: 500ms)
|
|
7
|
+
* @returns throttle된 값
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // 스크롤 위치 추적 최적화
|
|
11
|
+
* function ScrollTracker() {
|
|
12
|
+
* const [scrollY, setScrollY] = useState(0);
|
|
13
|
+
* const throttledScrollY = useThrottle(scrollY, 200);
|
|
14
|
+
*
|
|
15
|
+
* useEffect(() => {
|
|
16
|
+
* const handleScroll = () => setScrollY(window.scrollY);
|
|
17
|
+
* window.addEventListener('scroll', handleScroll);
|
|
18
|
+
* return () => window.removeEventListener('scroll', handleScroll);
|
|
19
|
+
* }, []);
|
|
20
|
+
*
|
|
21
|
+
* return <div>Scroll position: {throttledScrollY}px</div>;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // 무한 스크롤
|
|
26
|
+
* function InfiniteScroll() {
|
|
27
|
+
* const [scrollY, setScrollY] = useState(0);
|
|
28
|
+
* const throttledScrollY = useThrottle(scrollY, 300);
|
|
29
|
+
*
|
|
30
|
+
* useEffect(() => {
|
|
31
|
+
* const bottom = document.documentElement.scrollHeight - window.innerHeight;
|
|
32
|
+
* if (throttledScrollY >= bottom - 100) {
|
|
33
|
+
* loadMoreData();
|
|
34
|
+
* }
|
|
35
|
+
* }, [throttledScrollY]);
|
|
36
|
+
*
|
|
37
|
+
* return <div>...</div>;
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // 검색 입력 (실시간 검색에 적합)
|
|
42
|
+
* function LiveSearch() {
|
|
43
|
+
* const [query, setQuery] = useState('');
|
|
44
|
+
* const throttledQuery = useThrottle(query, 500);
|
|
45
|
+
*
|
|
46
|
+
* useEffect(() => {
|
|
47
|
+
* // 500ms마다 최대 한 번씩만 API 호출
|
|
48
|
+
* if (throttledQuery) {
|
|
49
|
+
* searchAPI(throttledQuery);
|
|
50
|
+
* }
|
|
51
|
+
* }, [throttledQuery]);
|
|
52
|
+
*
|
|
53
|
+
* return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
export declare function useThrottle<T>(value: T, interval?: number): T;
|
|
57
|
+
//# sourceMappingURL=useThrottle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useThrottle.d.ts","sourceRoot":"","sources":["../../src/hooks/useThrottle.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,GAAE,MAAY,GAAG,CAAC,CA0BlE"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 값의 업데이트를 일정 시간 간격으로 제한하는 hook
|
|
4
|
+
* debounce와 달리 일정 간격마다 최신 값을 업데이트합니다.
|
|
5
|
+
*
|
|
6
|
+
* @param value - throttle할 값
|
|
7
|
+
* @param interval - 업데이트 간격 (밀리초, 기본값: 500ms)
|
|
8
|
+
* @returns throttle된 값
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // 스크롤 위치 추적 최적화
|
|
12
|
+
* function ScrollTracker() {
|
|
13
|
+
* const [scrollY, setScrollY] = useState(0);
|
|
14
|
+
* const throttledScrollY = useThrottle(scrollY, 200);
|
|
15
|
+
*
|
|
16
|
+
* useEffect(() => {
|
|
17
|
+
* const handleScroll = () => setScrollY(window.scrollY);
|
|
18
|
+
* window.addEventListener('scroll', handleScroll);
|
|
19
|
+
* return () => window.removeEventListener('scroll', handleScroll);
|
|
20
|
+
* }, []);
|
|
21
|
+
*
|
|
22
|
+
* return <div>Scroll position: {throttledScrollY}px</div>;
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // 무한 스크롤
|
|
27
|
+
* function InfiniteScroll() {
|
|
28
|
+
* const [scrollY, setScrollY] = useState(0);
|
|
29
|
+
* const throttledScrollY = useThrottle(scrollY, 300);
|
|
30
|
+
*
|
|
31
|
+
* useEffect(() => {
|
|
32
|
+
* const bottom = document.documentElement.scrollHeight - window.innerHeight;
|
|
33
|
+
* if (throttledScrollY >= bottom - 100) {
|
|
34
|
+
* loadMoreData();
|
|
35
|
+
* }
|
|
36
|
+
* }, [throttledScrollY]);
|
|
37
|
+
*
|
|
38
|
+
* return <div>...</div>;
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // 검색 입력 (실시간 검색에 적합)
|
|
43
|
+
* function LiveSearch() {
|
|
44
|
+
* const [query, setQuery] = useState('');
|
|
45
|
+
* const throttledQuery = useThrottle(query, 500);
|
|
46
|
+
*
|
|
47
|
+
* useEffect(() => {
|
|
48
|
+
* // 500ms마다 최대 한 번씩만 API 호출
|
|
49
|
+
* if (throttledQuery) {
|
|
50
|
+
* searchAPI(throttledQuery);
|
|
51
|
+
* }
|
|
52
|
+
* }, [throttledQuery]);
|
|
53
|
+
*
|
|
54
|
+
* return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
export function useThrottle(value, interval = 500) {
|
|
58
|
+
const [throttledValue, setThrottledValue] = useState(value);
|
|
59
|
+
const lastExecuted = useRef(Date.now());
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
// 마지막 실행으로부터 경과된 시간
|
|
62
|
+
const timeSinceLastExecution = Date.now() - lastExecuted.current;
|
|
63
|
+
if (timeSinceLastExecution >= interval) {
|
|
64
|
+
// 간격이 지났으면 즉시 업데이트
|
|
65
|
+
setThrottledValue(value);
|
|
66
|
+
lastExecuted.current = Date.now();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// 아직 간격이 안 지났으면 남은 시간 후에 업데이트
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
setThrottledValue(value);
|
|
72
|
+
lastExecuted.current = Date.now();
|
|
73
|
+
}, interval - timeSinceLastExecution);
|
|
74
|
+
return () => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}, [value, interval]);
|
|
79
|
+
return throttledValue;
|
|
80
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* boolean 상태를 쉽게 토글할 수 있는 hook
|
|
3
|
+
*
|
|
4
|
+
* @param initialValue - 초기값 (기본값: false)
|
|
5
|
+
* @returns [현재 값, 토글 함수, 값 설정 함수]
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // 기본 사용
|
|
9
|
+
* function Modal() {
|
|
10
|
+
* const [isOpen, toggleOpen, setIsOpen] = useToggle(false);
|
|
11
|
+
*
|
|
12
|
+
* return (
|
|
13
|
+
* <>
|
|
14
|
+
* <button onClick={toggleOpen}>Toggle Modal</button>
|
|
15
|
+
* <button onClick={() => setIsOpen(true)}>Open Modal</button>
|
|
16
|
+
* <button onClick={() => setIsOpen(false)}>Close Modal</button>
|
|
17
|
+
* {isOpen && <div>Modal Content</div>}
|
|
18
|
+
* </>
|
|
19
|
+
* );
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // 다크모드 토글
|
|
24
|
+
* function ThemeToggle() {
|
|
25
|
+
* const [isDark, toggleTheme] = useToggle(false);
|
|
26
|
+
*
|
|
27
|
+
* return (
|
|
28
|
+
* <button onClick={toggleTheme}>
|
|
29
|
+
* {isDark ? '🌙 Dark' : '☀️ Light'}
|
|
30
|
+
* </button>
|
|
31
|
+
* );
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // 메뉴 열기/닫기
|
|
36
|
+
* function Sidebar() {
|
|
37
|
+
* const [isExpanded, toggleExpanded] = useToggle(true);
|
|
38
|
+
*
|
|
39
|
+
* return (
|
|
40
|
+
* <aside className={isExpanded ? 'expanded' : 'collapsed'}>
|
|
41
|
+
* <button onClick={toggleExpanded}>
|
|
42
|
+
* {isExpanded ? '◀' : '▶'}
|
|
43
|
+
* </button>
|
|
44
|
+
* </aside>
|
|
45
|
+
* );
|
|
46
|
+
* }
|
|
47
|
+
*/
|
|
48
|
+
export declare function useToggle(initialValue?: boolean): [boolean, () => void, (value: boolean) => void];
|
|
49
|
+
//# sourceMappingURL=useToggle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useToggle.d.ts","sourceRoot":"","sources":["../../src/hooks/useToggle.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,SAAS,CACvB,YAAY,GAAE,OAAe,GAC5B,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,CASjD"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* boolean 상태를 쉽게 토글할 수 있는 hook
|
|
4
|
+
*
|
|
5
|
+
* @param initialValue - 초기값 (기본값: false)
|
|
6
|
+
* @returns [현재 값, 토글 함수, 값 설정 함수]
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // 기본 사용
|
|
10
|
+
* function Modal() {
|
|
11
|
+
* const [isOpen, toggleOpen, setIsOpen] = useToggle(false);
|
|
12
|
+
*
|
|
13
|
+
* return (
|
|
14
|
+
* <>
|
|
15
|
+
* <button onClick={toggleOpen}>Toggle Modal</button>
|
|
16
|
+
* <button onClick={() => setIsOpen(true)}>Open Modal</button>
|
|
17
|
+
* <button onClick={() => setIsOpen(false)}>Close Modal</button>
|
|
18
|
+
* {isOpen && <div>Modal Content</div>}
|
|
19
|
+
* </>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // 다크모드 토글
|
|
25
|
+
* function ThemeToggle() {
|
|
26
|
+
* const [isDark, toggleTheme] = useToggle(false);
|
|
27
|
+
*
|
|
28
|
+
* return (
|
|
29
|
+
* <button onClick={toggleTheme}>
|
|
30
|
+
* {isDark ? '🌙 Dark' : '☀️ Light'}
|
|
31
|
+
* </button>
|
|
32
|
+
* );
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // 메뉴 열기/닫기
|
|
37
|
+
* function Sidebar() {
|
|
38
|
+
* const [isExpanded, toggleExpanded] = useToggle(true);
|
|
39
|
+
*
|
|
40
|
+
* return (
|
|
41
|
+
* <aside className={isExpanded ? 'expanded' : 'collapsed'}>
|
|
42
|
+
* <button onClick={toggleExpanded}>
|
|
43
|
+
* {isExpanded ? '◀' : '▶'}
|
|
44
|
+
* </button>
|
|
45
|
+
* </aside>
|
|
46
|
+
* );
|
|
47
|
+
* }
|
|
48
|
+
*/
|
|
49
|
+
export function useToggle(initialValue = false) {
|
|
50
|
+
const [value, setValue] = useState(initialValue);
|
|
51
|
+
// 토글 함수는 리렌더링 시에도 동일한 참조를 유지
|
|
52
|
+
const toggle = useCallback(() => {
|
|
53
|
+
setValue((prev) => !prev);
|
|
54
|
+
}, []);
|
|
55
|
+
return [value, toggle, setValue];
|
|
56
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 윈도우 크기 정보
|
|
3
|
+
*/
|
|
4
|
+
export interface WindowSize {
|
|
5
|
+
/** 윈도우 너비 (픽셀) */
|
|
6
|
+
width: number;
|
|
7
|
+
/** 윈도우 높이 (픽셀) */
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 윈도우 크기를 추적하는 hook
|
|
12
|
+
*
|
|
13
|
+
* @returns 윈도우의 현재 너비와 높이
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // 기본 사용
|
|
17
|
+
* function ResponsiveComponent() {
|
|
18
|
+
* const { width, height } = useWindowSize();
|
|
19
|
+
*
|
|
20
|
+
* return (
|
|
21
|
+
* <div>
|
|
22
|
+
* Window size: {width} x {height}
|
|
23
|
+
* </div>
|
|
24
|
+
* );
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // 반응형 레이아웃
|
|
29
|
+
* function AdaptiveLayout() {
|
|
30
|
+
* const { width } = useWindowSize();
|
|
31
|
+
*
|
|
32
|
+
* if (width < 768) {
|
|
33
|
+
* return <MobileLayout />;
|
|
34
|
+
* } else if (width < 1024) {
|
|
35
|
+
* return <TabletLayout />;
|
|
36
|
+
* } else {
|
|
37
|
+
* return <DesktopLayout />;
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // 캔버스 크기 조정
|
|
43
|
+
* function Canvas() {
|
|
44
|
+
* const { width, height } = useWindowSize();
|
|
45
|
+
* const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
46
|
+
*
|
|
47
|
+
* useEffect(() => {
|
|
48
|
+
* if (canvasRef.current) {
|
|
49
|
+
* canvasRef.current.width = width;
|
|
50
|
+
* canvasRef.current.height = height;
|
|
51
|
+
* }
|
|
52
|
+
* }, [width, height]);
|
|
53
|
+
*
|
|
54
|
+
* return <canvas ref={canvasRef} />;
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
export declare function useWindowSize(): WindowSize;
|
|
58
|
+
//# sourceMappingURL=useWindowSize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWindowSize.d.ts","sourceRoot":"","sources":["../../src/hooks/useWindowSize.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,kBAAkB;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAoC1C"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 윈도우 크기를 추적하는 hook
|
|
4
|
+
*
|
|
5
|
+
* @returns 윈도우의 현재 너비와 높이
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // 기본 사용
|
|
9
|
+
* function ResponsiveComponent() {
|
|
10
|
+
* const { width, height } = useWindowSize();
|
|
11
|
+
*
|
|
12
|
+
* return (
|
|
13
|
+
* <div>
|
|
14
|
+
* Window size: {width} x {height}
|
|
15
|
+
* </div>
|
|
16
|
+
* );
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // 반응형 레이아웃
|
|
21
|
+
* function AdaptiveLayout() {
|
|
22
|
+
* const { width } = useWindowSize();
|
|
23
|
+
*
|
|
24
|
+
* if (width < 768) {
|
|
25
|
+
* return <MobileLayout />;
|
|
26
|
+
* } else if (width < 1024) {
|
|
27
|
+
* return <TabletLayout />;
|
|
28
|
+
* } else {
|
|
29
|
+
* return <DesktopLayout />;
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // 캔버스 크기 조정
|
|
35
|
+
* function Canvas() {
|
|
36
|
+
* const { width, height } = useWindowSize();
|
|
37
|
+
* const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
38
|
+
*
|
|
39
|
+
* useEffect(() => {
|
|
40
|
+
* if (canvasRef.current) {
|
|
41
|
+
* canvasRef.current.width = width;
|
|
42
|
+
* canvasRef.current.height = height;
|
|
43
|
+
* }
|
|
44
|
+
* }, [width, height]);
|
|
45
|
+
*
|
|
46
|
+
* return <canvas ref={canvasRef} />;
|
|
47
|
+
* }
|
|
48
|
+
*/
|
|
49
|
+
export function useWindowSize() {
|
|
50
|
+
// SSR 환경에서는 기본값 사용
|
|
51
|
+
const [windowSize, setWindowSize] = useState(() => {
|
|
52
|
+
if (typeof window === 'undefined') {
|
|
53
|
+
return { width: 0, height: 0 };
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
width: window.innerWidth,
|
|
57
|
+
height: window.innerHeight,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
// SSR 환경에서는 이벤트 리스너 추가하지 않음
|
|
62
|
+
if (typeof window === 'undefined') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const handleResize = () => {
|
|
66
|
+
setWindowSize({
|
|
67
|
+
width: window.innerWidth,
|
|
68
|
+
height: window.innerHeight,
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
// 초기 크기 설정
|
|
72
|
+
handleResize();
|
|
73
|
+
window.addEventListener('resize', handleResize);
|
|
74
|
+
return () => {
|
|
75
|
+
window.removeEventListener('resize', handleResize);
|
|
76
|
+
};
|
|
77
|
+
}, []);
|
|
78
|
+
return windowSize;
|
|
79
|
+
}
|