Untitled

๐Ÿ˜‰ ํ—ท๊ฐˆ๋ ธ๋˜ & ๋ชฐ๋ž๋˜ ๋ถ€๋ถ„๋“ค๋งŒ ์ •๋ฆฌํ•˜๋Š” ๋‚˜๋งŒ์˜ TIL
๐Ÿ˜ฏ ๋ชจ๋“  ๊ฐ•์˜ ๋‚ด์šฉ์€ ์ ์ง€ ์•Š์•„์š”!

์˜ค๋Š˜์˜ ์†Œ๊ฐ์€?
์˜ค๋Š˜๋„ ์—ด์‹ฌํžˆ ์‚ฌ์šฉ์ž ์ •์˜ ํ›…๋“ค์— ๋Œ€ํ•ด ์‹ค์Šตํ–ˆ๋‹ค.
useInterval๊ณผ useTimeout์€ Fn(ํ•จ์ˆ˜) ํŒŒ์ผ๋“ค์„ ๋‚˜๋ˆ„์–ด ์ž‘์„ฑํ–ˆ๋‹ค.
์ฐจ์ด๋Š” ๊ฐ„๋‹จํ•˜๋‹ค. ์‹œ์ž‘์„ ๋ˆŒ๋Ÿฌ์ฃผ๋Š๋ƒ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋กœ๋“œ๋˜์ž๋งˆ์ž ์‹คํ–‰๋˜๋Š๋ƒ.

useDebounce๋Š” ํ–ฅํ›„ ์ œ์ž‘ํ•  ํ”„๋กœ์ ํŠธ์—์„œ๋„ ๊ต‰์žฅํžˆ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ๋  ๊ฒƒ ๊ฐ™๋‹ค.
useInterval์„ ์‚ฌ์šฉํ•˜๋ฉด ์“ฐ๋กœํ‹€๋ง ๋ฐฉ์‹๋„ Hook์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ.




[1] ์ปดํฌ๋„ŒํŠธ ์‹ค์Šต

useResize

// 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.stories.js
import styled from "@emotion/styled";
import { useState } from "react";
import Image from "../../components/_BasicComponent/Image";
import useResize from "../../hooks/useResize";

export default {
  title: "Hook/useResize",
};

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

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"
      />
    </Background>
  );
};



useLocalStorage, useSessionStorage

LocalStorage์™€ SessionStorage์ด์šฉ ๋ฐฉ๋ฒ•์€ ๋™์ผํ•˜๊ธฐ์—, ์ €์žฅ์†Œ ์ด๋ฆ„๋งŒ ๋ฐ”๊ฟ”์ฃผ๋ฉด ๋œ๋‹ค.

๋”ฐ๋ผ์„œ ์ฝ”๋“œ๋Š” useLocalStorage๋งŒ ์ฒจ๋ถ€ํ•œ๋‹ค.


// 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(valueToStore);
      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/useLocalStoreage",
};

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

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



useForm

์ •๊ทœ ํ‘œํ˜„์‹ ์–ด๋ ต๋‹คโ€ฆ.

๋” ๋ณต์žกํ•œ ์ •๊ทœ ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•œ๋‹ค๋Š”๋ฐ, ์•Œ์•„๋ณด๊ณ  ํ”„๋กœ์ ํŠธ์—์„œ ์ ์šฉํ•ด๋ด์•ผ๊ฒ ๋‹ค.


// useForm.js
import { useState } from "react";

const useForm = ({ initialValues, onSubmit, validate }) => {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [isLoading, setIsLoading] = useState(false);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues({ ...values, [name]: value });
  };

  const handleSubmit = async (e) => {
    setIsLoading(true);
    e.preventDefault();

    const newErrors = validate(values);

    if (Object.keys(newErrors).length === 0) {
      await onSubmit(values);
    }

    setErrors(newErrors);
    setIsLoading(false);
  };

  return {
    values,
    errors,
    isLoading,
    handleChange,
    handleSubmit,
  };
};

export default useForm;
// useForm.stories.js
import useForm from "../../hooks/useForm";

export default {
  title: "Hook/useForm",
};

const sleep = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(), 1000);
  });
};

export const Default = () => {
  const { isLoading, errors, handleChange, handleSubmit } = useForm({
    initialValues: {
      email: "",
      password: "",
    },
    onSubmit: async (values) => {
      await sleep();
      alert(JSON.stringify(values));
    },
    validate: ({ email, password }) => {
      const errors = {};
      if (!email) errors.email = "์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.";
      if (!password) errors.password = "๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.";
      if (!/^.+@.+\..+$/.test(email))
        errors.email = "์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.";

      return errors;
    },
  });
  return (
    <form onSubmit={handleSubmit}>
      <h1>Sign In</h1>
      <div>
        <input
          name="email"
          type="email"
          placeholder="email"
          onChange={handleChange}
        />
        {errors.email}
      </div>
      <div>
        <input
          name="password"
          type="password"
          placeholder="password"
          onChange={handleChange}
        />
        {errors.password}
      </div>
      <button type="submit" disabled={isLoading}>
        {isLoading ? "Loading..." : "Submit"}
      </button>
    </form>
  );
};



useTimeout

useTimeout์€ useTimeoutFnํ›…์„ ์‚ฌ์šฉํ•œ๋‹ค.


// useTimeoutFn.js
import { useRef, useEffect, useCallback } from "react";

const useTimeoutFn = (fn, ms) => {
  const timeoutId = useRef();
  const callback = useRef(fn);

  useEffect(() => {
    callback.current = fn;
  }, [fn]);

  const run = useCallback(() => {
    timeoutId.current && clearTimeout(timeoutId.current);

    timeoutId.current = setTimeout(() => {
      callback.current();
    }, ms);
  }, [ms]);

  const clear = useCallback(() => {
    timeoutId.current && clearTimeout(timeoutId.current);
  }, []);

  useEffect(() => clear, [clear]);

  return [run, clear];
};

export default useTimeoutFn;
// useTimeout.js
import { useEffect } from "react";
import useTimeoutFn from "./useTimeoutFn";

const useTimeout = (fn, ms) => {
  const [run, clear] = useTimeoutFn(fn, ms);

  useEffect(() => {
    run();
    return clear;
  }, [run, clear]);

  return clear;
};

export default useTimeout;
// useTimeoutFn.stories.js
import useTimeoutFn from "../../hooks/useTimeoutFn";

export default {
  title: "Hook/useTimeoutFn",
};

export const Default = () => {
  const [run, clear] = useTimeoutFn(() => {
    alert("์‹คํ–‰");
  }, 3000);
  return (
    <>
      <div>useTimeoutFn Test</div>
      <button onClick={run}>3์ดˆ ๋’ค ์‹คํ–‰</button>
      <button onClick={clear}>Stop</button>
    </>
  );
};
// useTimeout.stories.js
import useTimeout from "../../hooks/useTimeout";

export default {
  title: "Hook/useTimeout",
};

export const Default = () => {
  const clear = useTimeout(() => {
    alert("์‹คํ–‰");
  }, 3000);
  return (
    <>
      <div>useTimeout Test</div>
      <button onClick={clear}>Stop</button>
    </>
  );
};



useInterval

useInterval์€ useIntervalFnํ›…์„ ์‚ฌ์šฉํ•œ๋‹ค.


// useIntervalFn.js
import { useRef, useEffect, useCallback } from "react";

const useIntervalFn = (fn, ms) => {
  const intervalId = useRef();
  const callback = useRef(fn);

  useEffect(() => {
    callback.current = fn;
  }, [fn]);

  const run = useCallback(() => {
    intervalId.current && clearInterval(intervalId.current);

    intervalId.current = setInterval(() => {
      callback.current();
    }, ms);
  }, [ms]);

  const clear = useCallback(() => {
    intervalId.current && clearInterval(intervalId.current);
  }, []);

  useEffect(() => clear, [clear]);

  return [run, clear];
};

export default useIntervalFn;
// useInterval.js
import { useEffect } from "react";
import useIntervalFn from "./useIntervalFn";

const useInterval = (fn, ms) => {
  const [run, clear] = useIntervalFn(fn, ms);

  useEffect(() => {
    run();

    return clear;
  }, [run, clear]);

  return clear;
};

export default useInterval;
// useIntervalFn.stories.js
import { useState } from "react";
import useIntervalFn from "../../hooks/useIntervalFn";

export default {
  title: "Hook/useIntervalFn",
};

export const Default = () => {
  const [array, setArray] = useState([]);
  const [run, clear] = useIntervalFn(() => {
    setArray([...array, "Plus"]);
  }, 1000);

  return (
    <>
      <div>useIntervalFn Test</div>
      <div>{array}</div>
      <button onClick={run}>1์ดˆ๋งˆ๋‹ค ์ถ”๊ฐ€!</button>
      <button onClick={clear}>Clear</button>
    </>
  );
};
// useInterval.stories.js
import { useState } from "react";
import useInterval from "../../hooks/useInterval";

export default {
  title: "Hook/useInterval",
};

export const Default = () => {
  const [array, setArray] = useState([]);
  const clear = useInterval(() => {
    setArray([...array, "Plus"]);
  }, 1000);

  return (
    <>
      <div>useInterval Test</div>
      <div>{array}</div>
      <button onClick={clear}>Clear</button>
    </>
  );
};



useDebounce

eslint๊ฒฝ๊ณ ๊ฐ€ ๋– ์„œ disactive ์ฃผ์„์„ ๋„ฃ์–ด๋ฒ„๋ ธ๋‹ค.


// useDebounce.js
import { useEffect } from "react";
import useTimeoutFn from "./useTimeoutFn";

const useDebounce = (fn, ms, deps) => {
  const [run, clear] = useTimeoutFn(fn, ms);

  // eslint-disable-next-line react-hooks/exhaustive-deps
  useEffect(run, deps);

  return clear;
};

export default useDebounce;
// useDebounce.stories.js
import { Fragment, useState } from "react";
import useDebounce from "../../hooks/useDebounce";

export default {
  title: "Hook/useDebounce",
};

const companies = ["Kakao", "Naver", "Coupang", "Line", "Woowahan"];

export const Default = () => {
  const [value, setValue] = useState("");
  const [result, setResult] = useState([]);

  useDebounce(
    () => {
      if (value === "") setResult([]);
      else {
        setResult(
          companies.filter((company) =>
            company.toLowerCase().includes(value.toLowerCase())
          )
        );
      }
    },
    300,
    [value]
  );

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <div>
        {result.map((item) => (
          <Fragment key={item}>
            {item}
            <br />
          </Fragment>
        ))}
      </div>
    </div>
  );
};





์ถœ์ฒ˜

ํ”„๋กœ๊ทธ๋ž˜๋จธ์Šค

์นดํ…Œ๊ณ ๋ฆฌ: ,

์—…๋ฐ์ดํŠธ:

๋Œ“๊ธ€๋‚จ๊ธฐ๊ธฐ