[react] 커스텀 훅 연습 1

young-gue Park·2023년 3월 22일
0

React

목록 보기
12/17
post-thumbnail

⚡ 커스텀 훅 연습 1


📌 UseHover

  • 커서를 갖다대면 true, 커서가 벗어나면 false를 반환하는 훅

💻 useHover.js

import { useEffect, useRef, useState, useCallback } from "react";

const useHover = () => {
    const [state, setState] = useState(false);
    const ref = useRef(null);

    const handleMouseOver = useCallback(() => setState(true), []);
    const handleMouseOut = useCallback(() => setState(false), []);

    useEffect(() => {
        const element = ref.current;
        if(!element) return;

        element.addEventListener('mouseover', handleMouseOver);
        element.addEventListener('mouseout', handleMouseOut);

        return () => {
            element.removeEventListener('mouseover', handleMouseOver);
            element.removeEventListener('mouseout', handleMouseOut);
        }
    }, [ref, handleMouseOver, handleMouseOut]);

    return [ref, state];
}

export default useHover;

💻 useHover.stories.js

import styled from "@emotion/styled"
import useHover from "../../hooks/useHover";


export default {
    title: 'Hook/useHover',
}

const Box = styled.div`
    width: 100px;
    height: 100px;
    background-color: red;
`

export const Default = () => {
    const [ref, hover] = useHover();

    return (
        <>
            <Box ref={ref}/>
            {hover ? <div>ToolTip!</div> : null}
        </>
    )
}

🖨 완성된 훅 시연

이벤트 작동 전

빨간 사각형에 커서를 갖다 댔을 때

📌 useScroll

  • 특정 엘리먼트의 스크롤 위치를 추적하는 훅

  • useRafState
    - useScroll에서 scroll을 움직일 때마다 리렌더링 되는 것을 막는 성능 최적화 훅

💻 useRafState.js

import { useRef, useState, useCallback } from "react";

const useRafState = (initialState) => {
    const frame = useRef(0);
    const [state, setState] = useState(initialState);

    const setRafState = useCallback((value) => {
        cancelAnimationFrame(frame.current);

        frame.current = requestAnimationFrame(() => {
            setState(value);
        })
    }, []);
    
    return [state, setRafState];
};

export default useRafState;

💻 useScroll.js

import { useEffect, useRef } from "react"
import useRafState from "./useRafState";

const useScroll = () => {
    const [state, setState] = useRafState({ x: 0, y: 0 });
    const ref =useRef(null);

    useEffect(() => {
        const element = ref.current;
        if(!element) return;

        const handleScroll = () => {
            setState({
                x: ref.current.scrollLeft,
                y: ref.current.scrollTop
            })
        }

        // passive: true로 설정해놓을 경우 preventDefault의 경우와 같을 때 브라우저가 체크하지 않아 성능적인 이점을 가질 수 있다
        element.addEventListener('scroll', handleScroll, { passive: true });

        return () => {
            element.removeEventListener('scroll', handleScroll);
        }
    }, [ref, setState]);

    return [ref, state];
}

export default useScroll;

💻 useScroll.stories.js

import styled from "@emotion/styled"
import useScroll from "../../hooks/useScroll";


export default {
    title: 'Hook/useScroll',
}

const Box = styled.div`
    width: 100px;
    height: 100px;
    background-color: red;
    overflow: auto;
`

const Inner = styled.div`
    width: 10000px;
    height: 10000px;
    background-image: linear-gradient(180deg, #000 0%, #fff 100%);
`

export const Default = () => {
    const [ref, coord] = useScroll();

    return (
        <>
            <Box ref={ref}>
                <Inner></Inner>
            </Box>
            <button onClick={() => {
                ref.current.scrollTo({ top: 20000, left: 20000, behavior: 'smooth'})
            }}>scroll</button>
            {coord.x}, {coord.y}
        </>
    )
}

🖨 완성된 훅 시연

x좌표와 y좌표를 측정하여 표시하며 버튼을 누르면 끝까지 스크롤 된다.

📌 useKey, useKeyPress

  • 키보드 이벤트를 담당하는 훅, 주로 단축키를 담당

💻 useKey.js

import { useCallback, useEffect } from "react"

const useKey = (event = 'keydown', targetKey, handler) => {
    const handleKey = useCallback(({ key }) => {
        if(key===targetKey) {
            handler();
        }
    }, [targetKey, handler]);

    useEffect(() => {
        window.addEventListener(event, handleKey);

        return () => {
            window.removeEventListener(event, handleKey);
        }
    }, [event, targetKey, handleKey]);
}

export default useKey;

💻 useKey.stories.js

import useKey from "../../hooks/useKey";


export default {
    title: 'Hook/useKey',
}

export const Default = () => {
    useKey("keydown", "f", () => {
        alert("f key down");
    });

    useKey("keyup", "q", () => {
        alert("q key up");
    });

    return <>f와 q를 눌러보세요</>;
}

🖨 완성된 훅 시연

q를 눌렀을 때

💻 useKeyPress.js

import { useCallback, useEffect, useState } from "react";

const useKeyPress = (targetKey) => {
    const [keyPressed, setKeyPressed] = useState(false);

    const handleKeyDown = useCallback(({ key }) => {
        if(key === targetKey) {
            setKeyPressed(true);
        }
    }, [targetKey]);

    const handleKeyUp = useCallback(({ key }) => {
        if(key === targetKey) {
            setKeyPressed(false);
        }
    }, [targetKey]);

    useEffect(() => {
        window.addEventListener('keydown', handleKeyDown);
        window.addEventListener('keyup', handleKeyUp);

        return () => {
            window.removeEventListener('keydown', handleKeyDown);
            window.removeEventListener('keyup', handleKeyUp);
        }
    })

    return keyPressed;
}

export default useKeyPress;

💻 useKeyPress.stories.js

import useKeyPress from "../../hooks/useKeyPress";


export default {
    title: 'Hook/useKeyPress',
}

export const Default = () => {
    const pressed = useKeyPress('?');

    return <>{pressed ? '아이린 조 아' : '물음표를 눌러보세요'}</>;
}

🖨 완성된 훅 시연

이벤트 작동 전

shift+/ 를 눌렀을 때 (?를 눌렀을 때)

📌 useClickAway

  • 특정영역 외부를 클릭하면 이벤트를 발생시키는 훅

💻 useClickAway.js

import { useEffect, useRef } from "react";

// 모바일의 경우 touchstart
const events = ['mousedown', 'touchstart']

const useClickAway = (handler) => {
    const savedHandler = useRef(handler);
    const ref = useRef(null);

    // useClickAway 이벤트가 발생할 때마다 렌더링되지 않고 ref 값만 바뀌게 설정한다.
    useEffect(() => {
        savedHandler.current = handler;
    }, [handler])

    useEffect(() => {
        const element = ref.current;
        if(!element) return;

        const handleEvent = (e) => {
            !element.contains(e.target) && savedHandler.current(e);
        }

        for(const eventName of events) {
            document.addEventListener(eventName, handleEvent);
        }

        return () => {
            for(const eventName of events) {
                document.removeEventListener(eventName, handleEvent);
            }
        };
    }, [ref]);

    return ref;
}

export default useClickAway;

💻 useClickAway.stories.js

import styled from "@emotion/styled"
import { useState } from "react";
import useClickAway from "../../hooks/useClickAway";

export default {
    title: 'Hook/useClickAway',
}

const Popover = styled.div`
    width: 200px;
    height: 200px;
    border: 2px solid black;
    background-color: #eee;
`

export const Default = () => {
    const [show, setShow] = useState(false);
    const ref = useClickAway((e) => {
        if(e.target.tagName !=="BUTTON")
            setShow(false);
    })

    return (
        <div>
            <button onClick={() => setShow(true)}>Show</button>
            <Popover ref={ref} style={{display: show ? 'block' : 'none'}}>박스 바깥을 눌러봐요!</Popover>
        </div>
    )
}

🖨 완성된 훅 시연

버튼을 눌렀을 때

박스 바깥을 눌렀을 때 박스가 사라진다.

📌 useResize

  • target element의 크기가 변했을 때 이벤트를 실행하는 훅

💻 useResize.js

import { useEffect, useRef } from "react";

const useResize = (handler) => {
    const savedHandler = useRef(handler);
    const ref = useRef(null);

    useEffect(() => {
        savedHandler.current = handler;
    }, [handler]);

    useEffect(() => {
        const element = ref.current;
        if(!element) return;

        // 크기 변화를 추적
        const observer = new ResizeObserver((entries) => {
            savedHandler.current(entries[0].contentRect);
        });

        observer.observe(element);

        return () => {
            observer.disconnect();
        }
    }, [ref])

    return ref;
}

export default useResize;

💻 useResize.js

import styled from "@emotion/styled";
import { useState } from "react";
import Image from "../../components/Image";
import useResize from "../../hooks/useResize";

const Background = styled.div`
    width: 100%;
    height: 400px;
    background-color: blue;
`;

export default {
    title: 'Hook/useResize',
}

export const Default = () => {
    const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
    const ref = useResize(rect => {
        setImageSize({ width: rect.width, height: rect.height })
    }) 

    return (
        <Background ref={ref}>
            <Image 
            width={imageSize.width} 
            height={imageSize.height}
            src="https://picsum.photos/1000" 
            mode="contain"></Image>
        </Background>
    )
}

🖨 완성된 훅 시연

일반적인 화면

작은 스마트폰 화면일 때에도 사진이 자동으로 비율을 맞춘다.

📌 useLocalStorage, useSessionStorage

  • 로컬저장소와 세션저장소를 이용할 때 사용하는 훅

💻 useLocalStorage.js

import { useState } from "react";

const useLocalStorage = (key, initialValue) => {
    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch(error) {
            console.error(error);
            return initialValue;
        }
    });

    const setValue = (value) => {
        try {
            const valueToStore = typeof value === 'function' ? value(storedValue) : value;
            setStoredValue(value);
            localStorage.setItem(key, JSON.stringify(valueToStore));
        } catch (error) {
            console.error(error);
        }
    };

    return [storedValue, setValue];
};

export default useLocalStorage;

💻 useLocalStorage.stories.js

import useLocalStorage from "../../hooks/useLocalStorage";

export default {
    title: 'Hook/useLocalStorage',
}

export const Default = () => {
    const [status, setStatus] = useLocalStorage('status', '404 NOT FOUND');

    return (
        <div>
            <button onClick={() => setStatus("200 OK")}>Resend</button>
            {status}
        </div>
    )
}

💻 useSessionStorage.js

import { useState } from "react";

const useSessionStorage = (key, initialValue) => {
    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = sessionStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch(error) {
            console.error(error);
            return initialValue;
        }
    });

    const setValue = (value) => {
        try {
            const valueToStore = typeof value === 'function' ? value(storedValue) : value;
            setStoredValue(value);
            sessionStorage.setItem(key, JSON.stringify(valueToStore));
        } catch (error) {
            console.error(error);
        }
    };

    return [storedValue, setValue];
};

export default useSessionStorage;

💻 useSessionStorage.stories.js

import useSessionStorage from "../../hooks/useSessionStorage";

export default {
    title: 'Hook/useSessionStorage',
}

export const Default = () => {
    const [status, setStatus] = useSessionStorage('status', '404 NOT FOUND');

    return (
        <div>
            <button onClick={() => setStatus("200 OK")}>Resend</button>
            {status}
        </div>
    )
}

🖨 완성된 훅 시연

두 훅 모두 버튼을 누르면 각각 로컬 저장소와 세션 저장소에 200 OK 상태로 정보가 저장되어 페이지를 닫아도 유지된다.

커스텀 훅을 만들어가며 이 훅들을 프로젝트에 어서 적용하고 싶다는 생각이 든다.

profile
Hodie mihi, Cras tibi

0개의 댓글