React Use 라이브러리 1편

이수빈·2024년 4월 10일
0

React

목록 보기
18/20
post-thumbnail

React Use란?

  • React use 라이브러리는 react에서 유용한 hook들을 사용 할 수 있게 해주는 라이브러리이다.

  • 공식문서 링크는 다음을 참고한다 : https://www.reactuse.com/

  • 많이 사용하는 것 정리와 더불어 구현원리를 알아보기 위해 코드를 공부해보자.

  • 공식문서에서는 4가지 카테고리 (State, Effect, Element, Browser)로 나눠서 hook들을 나눠놨다.

State

useCookie

  • 배열형태로 3가지 값을 반환한다.
  const [cookieValue, updateCookie, refreshCookie] = useCookie(
    cookieName,
    defaultOption,
    "default-value"
  );
  • 내부코드는 다음과 같다

  • 로직은 어려운건 없고, 특이한점은 쿠키관련 값을 다룰 때 js-cookie 라이브러리를 사용하고, string 값인지 check한다는 점

import { useCallback, useEffect, useState } from "react";
import Cookies from "js-cookie";
import { isBrowser, isFunction, isString } from "../utils/is";
import { defaultOptions } from "../utils/defaults";
import type { UseCookie, UseCookieState } from "./interface";

const getInitialState = (key: string, defaultValue?: string) => {
  // Prevent a React hydration mismatch when a default value is provided.
  if (defaultValue !== undefined) {
    return defaultValue;
  }

  if (isBrowser) {
    return Cookies.get(key);
  }

  if (process.env.NODE_ENV !== "production") {
    console.warn(
      "`useCookie` When server side rendering, defaultValue should be defined to prevent a hydration mismatches.",
    );
  }

  return "";
};

export const useCookie: UseCookie = (
  key: string,
  options: Cookies.CookieAttributes = defaultOptions,
  defaultValue?: string,
) => {
  const [cookieValue, setCookieValue] = useState<UseCookieState>(
    getInitialState(key, defaultValue),
  );

  useEffect(() => {
    const getStoredValue = () => {
      const raw = Cookies.get(key);
      if (raw !== undefined && raw !== null) {
        return raw;
      }
      else {
        if (defaultValue === undefined) {
          Cookies.remove(key);
        }
        else {
          Cookies.set(key, defaultValue, options);
        }
        return defaultValue;
      }
    };

    setCookieValue(getStoredValue());
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [defaultValue, key, JSON.stringify(options)]);

  const updateCookie = useCallback(
    (
      newValue: UseCookieState | ((prevState: UseCookieState) => UseCookieState),
    ) => {
      const value = isFunction(newValue) ? newValue(cookieValue) : newValue;

      if (value === undefined) {
        Cookies.remove(key);
      }
      else {
        Cookies.set(key, value, options);
      }

      setCookieValue(value);
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [key, cookieValue, JSON.stringify(options)],
  );

  const refreshCookie = useCallback(() => {
    const cookieValue = Cookies.get(key);

    if (isString(cookieValue)) {
      setCookieValue(cookieValue);
    }
  }, [key]);

  return [cookieValue, updateCookie, refreshCookie] as const;
};
  • js 에서 cookie값을 가져오려면 document.cookie 로 접근해서 직접 값을 파싱해야한다.

  • document.cookie에는 현재 문서와 관련된 모든 쿠키가 세미콜론으로 구분된 목록이 포함되어 있고,

  • 각 쿠키를 반복하여 제공된 이름과 일치하는 지 확인해서 해당값을 찾는 과정이 필요함.

function getCookie(cookieName) {
    // Split all cookies into an array
    const cookies = document.cookie.split(';');
    
    // Loop through each cookie to find the one with the specified name
    for (let i = 0; i < cookies.length; i++) {
        let cookie = cookies[i];
        // Remove leading spaces if any
        while (cookie.charAt(0) == ' ') {
            cookie = cookie.substring(1);
        }
        // Check if this cookie has the name we're looking for
        if (cookie.indexOf(cookieName + '=') == 0) {
            // Return the value of the cookie
            return cookie.substring(cookieName.length + 1, cookie.length);
        }
    }
    // Return null if cookie not found
    return null;
}

useCountDown, useCounter

  • useCountDown은 특정시간의 차이만큼 countDown 할 수 있도록, hour, minute, second를 반환해주는 hook이고,

  • useCounter은 counter를 손쉽게 만들어 줄 수 있게 도와주는 hook이다.

//useCountDown 
import { useEffect, useState } from "react";
import { useInterval } from "../useInterval";
import type { UseCountDown } from "./interface";

const padZero = (time: number): string => {
  return `${time}`.length < 2 ? `0${time}` : `${time}`;
};

export const getHMSTime = (timeDiff: number): [string, string, string] => {
  if (timeDiff <= 0) {
    return ["00", "00", "00"];
  }
  if (timeDiff > 100 * 3600) {
    return ["99", "59", "59"];
  }
  const hour = Math.floor(timeDiff / 3600);
  const minute = Math.floor((timeDiff - hour * 3600) / 60);
  const second = timeDiff - hour * 3600 - minute * 60;
  return [padZero(hour), padZero(minute), padZero(second)];
};

export const useCountDown: UseCountDown = (
  time: number,
  format: (num: number) => [string, string, string] = getHMSTime, //형식에 맞춰서 
  callback?: () => void,
) => {
  const [remainTime, setRemainTime] = useState(time);
  const [delay, setDelay] = useState<number | null>(1000);

  useInterval(() => {
    if (remainTime <= 0) {
      setDelay(null);
      return;
    }
    setRemainTime(remainTime - 1);
  }, delay); // interval로 state 감소

  useEffect(() => {
    if (time > 0 && remainTime <= 0) {
      callback && callback();
    }
  }, [callback, remainTime, time]);

  const [hour, minute, secoud] = format(remainTime); 
  // state가 변하면 => format에 맞춰 hour, minute, second변함

  return [hour, minute, secoud] as const;
};
  • useCounter

  • 초기화 지연 기능을 사용함 (initValue값을 함수로 받을 수도 있게 함) => 이를 위해 typeof === function으로 타입가드 후 각각에 맞춰서 로직 진행

import { useState } from "react";
import { useEvent } from "../useEvent";
import type { UseCounter } from "./interface";

export const useCounter: UseCounter = (
  initialValue: number | (() => number) = 0,
  max: number | null = null,
  min: number | null = null,
) => {
  // avoid exec init code every render
  const initFunc = () => {
    let init
      = typeof initialValue === "function" ? initialValue() : initialValue;

    typeof init !== "number"
      && console.error(
        `initialValue has to be a number, got ${typeof initialValue}`,
      );

    if (typeof min === "number") {
      init = Math.max(init, min);
    }
    else if (min !== null) {
      console.error(`min has to be a number, got ${typeof min}`);
    }

    if (typeof max === "number") {
      init = Math.min(init, max);
    }
    else if (max !== null) {
      console.error(`max has to be a number, got ${typeof max}`);
    }

    return init;
  };

  const [value, setValue] = useState(initFunc);

  const set = useEvent(
    (newState: number | ((prev: number) => number) | (() => number)) => {
      setValue((v) => {
        let nextValue = typeof newState === "function" ? newState(v) : newState;

        if (typeof min === "number") {
          nextValue = Math.max(nextValue, min);
        }
        if (typeof max === "number") {
          nextValue = Math.min(nextValue, max);
        }
        return nextValue;
      });
    },
  );

  const inc = (delta = 1) => {
    set(value => value + delta);
  };

  const dec = (delta = 1) => {
    set(value => value - delta);
  };

  const reset = () => {
    set(initFunc);
  };

  return [value, set, inc, dec, reset] as const;
};

useDebounce, useDebounceFn, useLatest

  • Throttle 와 Debounce 는 자주 사용 되는 이벤트나 함수 들의 실행되는 빈도를 줄여서, 성능 상의 유리함을 가져오기 위한 개념

  • input에 사용자가 입력한 값을 바탕으로 => api 호출이 필요하다고 가정할 때

차이점
Throttle 는 입력 주기를 방해하지 않고, 일정 시간 동안의 입력을 모와서, 한번씩 출력을 제한한다.
Debounce 는 입력 주기가 끝나면, 출력한다.

ex)

  • throttle 에서는 키보드입력 발생하면, 500ms 후에, 가장 최신 value 를 출력하고, 초기화 하여, 키보드 입력이 끝날때까지 반복한다.

  • debounce 에서는 키보드 입력이 발생하면, 500ms 동안 기다리다, 그 안에 키보드 입력이 발생하면, 시간을 초기화 하고 다시 기다리다, 가장 최신 value 를 출력한다.

  • useDebounce에서는 useDebounceFn이라는 hook을 사용

  • useDebounceFn에 => 1. value를 setting 할 수 있는 setter와 wait값, DebounceSettings option값을 넘겨준다.

https://www.reactuse.com/docs/state/useDebounce
https://www.reactuse.com/docs/effect/useDebounceFn

//이런식으로 사용하고
const debouncedValue = useDebounce(value, 500);

const { run } = useDebounceFn(() => {
    setValue(value + 1);
  }, 500);


import { useEffect, useState } from "react";
import type { DebounceSettings } from "lodash-es";
import { useDebounceFn } from "../useDebounceFn";
import type { UseDebounce } from "./interface";

export const useDebounce: UseDebounce = <T>(
  value: T,
  wait?: number,
  options?: DebounceSettings,
) => {
  const [debounced, setDebounced] = useState(value);

  const { run } = useDebounceFn(
    () => {
      setDebounced(value);
    },
    wait,
    options,
  );

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

  return debounced;
};
  • useDebounceFn은 lodash에 debounce를 사용한다.

  • 여기서 useLatest는 가장 최근에 state의 ref값에 직접 접근하는 hook이다. 밑에서 알아보자.

  • useLatest를 이용해 fn에 ref에 직접 접근한뒤, 다시 lodash의 debounce로 값들을 넘긴다.


import { useMemo } from "react";
import type { DebounceSettings } from "lodash-es";
import { debounce } from "lodash-es";
import { isDev, isFunction } from "../utils/is";
import { useLatest } from "../useLatest";
import { useUnmount } from "../useUnmount";
import type { UseDebounceFn } from "./interface";

export const useDebounceFn: UseDebounceFn = <T extends (...args: any) => any>(
  fn: T,
  wait?: number,
  options?: DebounceSettings,
) => {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(
        `useDebounceFn expected parameter is a function, got ${typeof fn}`,
      );
    }
  }

  const fnRef = useLatest(fn);

  const debounced = useMemo(
    () =>
      debounce(
        (...args: [...Parameters<T>]): ReturnType<T> => {
          return fnRef.current(...args);
        },
        wait,
        options,
      ),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [JSON.stringify(options), wait],
  );

  useUnmount(() => {
    debounced.cancel();
  });

  return {
    run: debounced,
    cancel: debounced.cancel,
    flush: debounced.flush,
  };
};
  • lodash의 debounce는 링크를 걸어둔다

https://github.com/lodash/lodash/blob/main/src/debounce.ts

  • 간단하게 구현 원리만 파악해본다 .
function debounced(...args) {
    const time = Date.now();
    const isInvoking = shouldInvoke(time);

    lastArgs = args;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking) {
        if (timerId === undefined) {
            return leadingEdge(lastCallTime);
        }
        if (maxing) {
            // Handle invocations in a tight loop.
            timerId = startTimer(timerExpired, wait);
            return invokeFunc(lastCallTime);
        }
    }
    if (timerId === undefined) {
        timerId = startTimer(timerExpired, wait);
    }
    return result;
}
  • 상태 관리: debounced 함수는 lastArgs, lastThis, lastCallTime 및 timerId와 같은 특정 상태 변수를 유지한다.

  • timerId => setTimeout timerId담는 변수, lastCallTime : 지난번에 호출된 시간

  • 마지막 호출 이후 경과된 시간을 기준으로 함수를 즉시 호출해야 하는지(isInvoking) 여부를 계산함.

=> isInvoking값이 true라면 즉시 호출해야 하는경우 => 하지만 어떤 delay내에 함수가 여러번 호출되었을 경우가 있어 예외처리를 해줘야함.

  • 활성 타이머가 없으면 즉시 함수를 호출합니다(leadingEdge) => function bind해서 값을 return하는 함수

  • 'maxing'도 true인 경우 새 타이머를 설정하고 함수를 다시 호출하여 디바운스 기간 동안 여러 호출이 이루어진 경우를 처리함.

  • 활성 타이머가 없는 경우(timerId가 정의되지 않음) 함수는 startTimer를 사용하여 새 타이머를 설정합니다. 이렇게 하면 마지막 호출 이후 디바운스 기간이 경과한 후에 디바운스된 함수가 호출됨.

useFirstMountState, useMountedState

  • useFirstMountState : 처음 mount된 state에 대한 상태값을 boolean으로 반환 (컴포넌트 상태값)
function Demo() {
  const isFirstMount = useFirstMountState();
  const [render, reRender] = useState(0);

  return (
    <div>
      <span>This component is just mounted: {isFirstMount ? "YES" : "NO"}</span>
      <br />
      <button onClick={() => reRender(1)}>{render}</button>
    </div>
  );
};

// useMountState.index.ts

import { useRef } from "react";
import type { UseFirstMountState } from "./interface";

export const useFirstMountState: UseFirstMountState = (): boolean => {
  const isFirst = useRef(true);

  if (isFirst.current) {
    isFirst.current = false;

    return true;
  }

  return isFirst.current;
};
  • useMountedState : 컴포넌트 mount 여부를 확인해서 boolean 값으로 반환
function Demo() {
  const isMounted = useMountedState();

  const [, update] = useState(0);
  useEffect(() => {
    update(1);
  }, []);
  return <div>This component is {isMounted() ? "MOUNTED" : "NOT MOUNTED"}</div>;
};


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

export const useMountedState = (): () => boolean => {
  const mountedRef = useRef<boolean>(false);
  const get = useCallback(() => mountedRef.current, []);

  useEffect(() => {
    mountedRef.current = true;

    return () => {
      mountedRef.current = false;
    };
  }, []);

  return get;
};

ref) https://www.reactuse.com/

https://pks2974.medium.com/throttle-%EC%99%80-debounce-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0-2335a9c426ff

profile
응애 나 애기 개발자

0개의 댓글