027

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

์˜ค๋Š˜์˜ ์†Œ๊ฐ์€?
๋“œ๋””์–ด ๋‹ค์Œ์ฃผ๋ถ€ํ„ฐ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘์ด๋‹ค.
๋–จ๋ฆผ ๋ฐ˜ + ๋ฆฌ์—‘ํŠธ์— ๋Œ€ํ•œ ๋ฏธ์ˆ™ํ•จ์œผ๋กœ ์ธํ•œ ๋‘๋ ค์›€ ๋ฐ˜์˜ ์ƒํƒœ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•œ๋‹ค.
์—ด์‹ฌํžˆ ๋‹ฌ๋ ค๋ณด์ž๊ณ ~!




[1] ์‚ฌ์šฉ์ž ์ •์˜ Hook

useAsync

useAsync ๋˜ํ•œ ์ €๋ฒˆ ์‹ค์Šต๊ณผ ๋™์ผํ•˜๊ฒŒ useAsyncFn๊ณผ ๋‚˜๋ˆ„์–ด ์ž‘์„ฑํ–ˆ๋‹ค.


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

const useAsyncFn = (fn, deps) => {
  const lastCallId = useRef(0);
  const [state, setState] = useState({
    isLoading: false,
  });

  const callback = useCallback((...args) => {
    const callId = ++lastCallId.current;

    if (!state.isLoading) {
      setState({ ...state, isLoading: true });
    }

    return fn(...args).then(
      (value) => {
        callId === lastCallId.current && setState({ value, isLoading: false });
        return value;
      },
      (error) => {
        callId === lastCallId.current && setState({ error, isLoading: false });
        return error;
      }
    );
  }, deps);

  return [state, callback];
};

export default useAsyncFn;
// useAsync
import { useEffect } from "react";
import useAsyncFn from "./usdAsyncFn";

const useAsync = (fn, deps) => {
  const [state, callback] = useAsyncFn(fn, deps);

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

  return state;
};

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

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

const asyncReturnValue = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("Success");
    }, 1000);
  });
};

const asyncReturnError = () => {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject("Failed");
    }, 1000);
  });
};

export const Success = () => {
  const state = useAsync(async () => {
    return await asyncReturnValue();
  }, []);

  return (
    <div>
      <div>useAsyncFn Test</div>
      <div>{JSON.stringify(state)}</div>
    </div>
  );
};

export const Error = () => {
  const state = useAsync(async () => {
    return await asyncReturnError();
  }, []);

  return (
    <div>
      <div>useAsyncFn Test</div>
      <div>{JSON.stringify(state)}</div>
    </div>
  );
};



useHotKey

ํ•ด๋‹น ์‹ค์Šต์ด ์ œ์ผ ์–ด๋ ค์› ๋‹ค.

์—ฌ๋Ÿฌ ํ‚ค ์กฐํ•ฉ์„ ํ’€์–ด ์ €์žฅํ•˜๋Š” ๊ณผ์ •์„ ์ดํ•ดํ•˜๋Š”๋ฐ ์‹œ๊ฐ„์ด ๋งŽ์ด ๊ฑธ๋ ธ๋˜ ๊ฒƒ ๊ฐ™๋‹ค.


// useHotKey.js
import { useCallback, useEffect, useMemo } from "react";

const ModifierBitMasks = {
  alt: 1,
  ctrl: 2,
  meta: 4,
  shift: 8,
};

const ShiftKeys = {
  "~": "`",
  "!": "1",
  "@": "2",
  "#": "3",
  $: "4",
  "%": "5",
  "^": "6",
  "&": "7",
  "*": "8",
  "(": "9",
  ")": "0",
  _: "-",
  "+": "=",
  "{": "[",
  "}": "]",
  "|": "\\",
  ":": ";",
  '"': "'",
  "<": ",",
  ">": ".",
  "?": "/",
};

const Aliases = {
  win: "meta",
  window: "meta",
  cmd: "meta",
  command: "meta",
  esc: "escape",
  opt: "alt",
  option: "alt",
};

const getKeyCombo = (e) => {
  const key = e.key !== " " ? e.key.toLowerCase() : "space";

  let modifiers = 0;
  if (e.altKey) modifiers += ModifierBitMasks.alt;
  if (e.ctrlKey) modifiers += ModifierBitMasks.ctrl;
  if (e.metaKey) modifiers += ModifierBitMasks.meta;
  if (e.shiftKey) modifiers += ModifierBitMasks.shift;

  return { modifiers, key };
};

const parseKeyCombo = (combo) => {
  const pieces = combo.replace(/\s/g, "").toLowerCase().split("+");
  let modifiers = 0;
  let key;
  for (const piece of pieces) {
    if (ModifierBitMasks[piece]) {
      modifiers += ModifierBitMasks[piece];
    } else if (ShiftKeys[piece]) {
      modifiers += ModifierBitMasks.shift;
      key = ShiftKeys[piece];
    } else if (Aliases[piece]) {
      key = Aliases[piece];
    } else {
      key = piece;
    }
  }

  return { modifiers, key };
};

const comboMatches = (a, b) => {
  return a.modifiers === b.modifiers && a.key === b.key;
};

const useHotKey = (hotkeys) => {
  const localKeys = useMemo(() => hotkeys.filter((k) => !k.global), [hotkeys]);
  const globalKeys = useMemo(() => hotkeys.filter((k) => k.global), [hotkeys]);

  const invokeCallback = useCallback(
    (global, combo, callbackName, e) => {
      for (const hotkey of global ? globalKeys : localKeys) {
        if (comboMatches(parseKeyCombo(hotkey.combo), combo)) {
          hotkey[callbackName] && hotkey[callbackName](e);
        }
      }
    },
    [localKeys, globalKeys]
  );

  const handleGlobalKeyDown = useCallback(
    (e) => {
      invokeCallback(true, getKeyCombo(e), "onKeyDown", e);
    },
    [invokeCallback]
  );

  const handleGlobalKeyUp = useCallback(
    (e) => {
      invokeCallback(true, getKeyCombo(e), "onKeyUp", e);
    },
    [invokeCallback]
  );

  const handleLocalKeyDown = useCallback(
    (e) => {
      invokeCallback(
        false,
        getKeyCombo(e.nativeEvent),
        "onKeyDown",
        e.nativeEvent
      );
    },
    [invokeCallback]
  );

  const handleLocalKeyUp = useCallback(
    (e) => {
      invokeCallback(
        false,
        getKeyCombo(e.nativeEvent),
        "onKeyUp",
        e.nativeEvent
      );
    },
    [invokeCallback]
  );

  useEffect(() => {
    document.addEventListener("keydown", handleGlobalKeyDown);
    document.addEventListener("keyup", handleGlobalKeyUp);

    return () => {
      document.removeEventListener("keydown", handleGlobalKeyDown);
      document.removeEventListener("keyup", handleGlobalKeyUp);
    };
  }, [handleGlobalKeyDown, handleGlobalKeyUp]);

  return { handleKeyDown: handleLocalKeyDown, handleKeyUp: handleLocalKeyUp };
};

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

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

export const Default = () => {
  const [value, setValue] = useState("");
  const hotkeys = [
    {
      global: true,
      combo: "meta+shift+k",
      onKeyDown: (e) => {
        alert("meta+shift+k");
      },
    },
    {
      combo: "esc",
      onKeyDown: (e) => {
        setValue("");
      },
    },
  ];

  const { handleKeyDown } = useHotKey(hotkeys);

  return (
    <div>
      <div>useHotKey ํ…Œ์ŠคํŠธ</div>
      <input
        onKeyDown={handleKeyDown}
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
    </div>
  );
};




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


// Modal.js
import styled from "@emotion/styled";
import { useMemo, useEffect } from "react";
import ReactDOM from "react-dom";
import useClickAway from "../../../hooks/useClickAway";

const BackgroundDim = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 1000;
`;

const ModalContainer = styled.div`
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 8px;
  background-color: white;
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
  box-sizing: border-box;
`;

const Modal = ({
  children,
  width = 500,
  height,
  visible = false,
  onClose,
  ...props
}) => {
  const ref = useClickAway(() => {
    onClose && onClose();
  });

  const containerStyle = useMemo(
    () => ({
      width,
      height,
    }),
    [width, height]
  );

  const el = useMemo(() => document.createElement("div"), []);
  useEffect(() => {
    document.body.appendChild(el);
    return () => {
      document.body.removeChild(el);
    };
  });

  return ReactDOM.createPortal(
    <BackgroundDim style={{ display: visible ? "block" : "none" }}>
      <ModalContainer
        {...props}
        style={{ ...props.style, ...containerStyle }}
        ref={ref}
      >
        {children}
      </ModalContainer>
    </BackgroundDim>,
    el
  );
};

export default Modal;

// Modal.stories.js
import Modal from "../../components/_BasicComponent/Modal";
import { useState } from "react";

export default {
  title: "Component/Modal",
  component: Modal,
};

export const Default = () => {
  const [visible, setVisible] = useState(false);
  return (
    <div>
      <button onClick={() => setVisible(true)}>Show Modal!</button>
      <Modal visible={visible} onClose={() => setVisible(false)}>
        Hi
      </Modal>
    </div>
  );
};

Toast

Toast ์ปดํฌ๋„ŒํŠธ๋Š” Toast ์•„์ดํ…œ๋“ค์„ ์ €์žฅํ•˜๋Š” ToastItem, ๋™์ž‘์„ ๊ด€๋ฆฌํ•˜๋Š” ToastManager๋กœ ๋‚˜๋ˆ„์–ด ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.


// ToastItem.js
import Text from "../Text";
import useTimeout from "../../../hooks/useTimeout";
import styled from "@emotion/styled";
import { useState } from "react";

const Container = styled.div`
  position: relative;
  display: flex;
  width: 450px;
  height: 70px;
  padding: 0 20px;
  align-items: center;
  border-radius: 4px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  border: 1px solid #ccc;
  background-color: white;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  box-sizing: border-box;
  opacity: 1;
  transition: opacity 0.4s ease-out;
  &:first-of-type {
    animation: move 0.4s ease-out forwards;
  }
  &:not(:first-of-type) {
    margin-top: 8px;
  }
  @keyframes move {
    0% {
      margin-top: 80px;
    }
    100% {
      margin-top: 0;
    }
  }
`;

const ProgressBar = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 4px;
  background-color: #44b;
  animation-name: progress;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  @keyframes progress {
    0% {
      width: 0;
    }
    100% {
      width: 100%;
    }
  }
`;

const ToastItem = ({ id, message, duration, onDone }) => {
  const [show, setShow] = useState(true);

  useTimeout(() => {
    setShow(false);
    setTimeout(() => onDone(), 400);
  }, duration);

  return (
    <Container style={{ opacity: show ? 1 : 0 }}>
      <ProgressBar style={{ animationDuration: `${duration}ms` }} />
      <Text>{message}</Text>
    </Container>
  );
};

export default ToastItem;

// ToastManager.js
import styled from "@emotion/styled";
import { useCallback, useState, useEffect } from "react";
import { v4 } from "uuid";
import ToastItem from "./ToastItem";

const Container = styled.div`
  position: fixed;
  top: 16px;
  right: 16px;
  z-index: 1500;
`;

const ToastManager = ({ bind }) => {
  const [toasts, setToasts] = useState([]);

  const createToast = useCallback((message, duration) => {
    const newToast = {
      id: v4(),
      message,
      duration,
    };
    setToasts((oldToasts) => [...oldToasts, newToast]);
  }, []);

  const removeToast = useCallback((id) => {
    setToasts((oldToasts) => oldToasts.filter((toast) => toast.id !== id));
  }, []);

  useEffect(() => {
    bind(createToast);
  }, [bind, createToast]);

  return (
    <Container>
      {toasts.map(({ id, message, duration }) => (
        <ToastItem
          message={message}
          duration={duration}
          key={id}
          onDone={() => removeToast(id)}
        />
      ))}
    </Container>
  );
};

export default ToastManager;
// index.js
import ReactDOM from "react-dom";
import ToastManager from "./ToastManager";

// ํ† ์ŠคํŠธ ๊ด€๋ฆฌ ๋ฐ ํ† ์ŠคํŠธ ๋„์šฐ๋Š” ์—ญํ•  Class
class Toast {
  portal = null;

  constructor() {
    const portalId = "toast-portal";
    const portalElement = document.getElementById(portalId);

    if (portalElement) {
      this.portal = portalElement;
      return;
    } else {
      this.portal = document.createElement("div");
      this.portal.id = portalId;
      document.body.appendChild(this.portal);
    }

    ReactDOM.render(
      <ToastManager
        bind={(createToast) => {
          this.createToast = createToast;
        }}
      />,
      this.portal
    );
  }

  show(message, duration = 2000) {
    this.createToast(message, duration);
  }
}

export default new Toast();
// Toast.stories.js
import Toast from "../../components/_BasicComponent/Toast";

export default {
  title: "Component/Toast",
};

export const Default = () => {
  return <button onClick={() => Toast.show("Hello!", 3000)}>Show Toast</button>;
};




[3] ๊ทธ๋ฆผํŒ ์‹ค์Šต

canvas๋ฅผ ์‚ฌ์šฉํ•ด Paint ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ œ์ž‘ํ•˜๊ณ  Plugins๋ฅผ ์ƒ์„ฑํ–ˆ๋‹ค.

Untitled

ํ™•์žฅ์„ฑ ์žˆ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.

๋‚˜์ค‘์—” ์ง„์งœ ๊ทธ๋ฆผํŒ์ฒ˜๋Ÿผ ๋‹ค๋ฅธ ํŽœ๋“ค๋„ ์ถ”๊ฐ€ํ•ด๋ด์•ผ์ง€.





์ถœ์ฒ˜

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

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

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

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