Untitled

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

์˜ค๋Š˜์˜ ์†Œ๊ฐ์€?
Notion ํด๋กœ๋‹ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ณ„์† ๋ฆฌํŒฉํ† ๋ง ์ค‘์ด๋‹ค.
์ง€๊ธˆ์€ API๋ฅผ ๊ฐˆ์•„๋ผ์› ๋‹ค ๐Ÿ˜‰
ํ•˜์ง€๋งŒ ์•„์ง https ์„ค์ •์„ ๋ชปํ•ด ๋กœ์ปฌ์—์„œ๋งŒ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๋‹ค. ํ•˜ํ•˜ํ•˜..

Modal ์ฐฝ ์™ธ๋ถ€๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ, ์ฐฝ์ด ๊บผ์ง€๋Š” ๊ธฐ๋Šฅ์„ Hook์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ตฌ๋‚˜.
React ์ปดํฌ๋„ŒํŠธ์™€ ํ›…์˜ ๋‹จ์œ„๋Š” ๋ฐฐ์šธ์ˆ˜๋ก ์‹ ๊ธฐํ•˜๋‹ค..

๊ณ ์–‘์ด ์‚ฌ์ง„์ฒฉ ๋•Œ ๋‚  ๊ดด๋กญํ˜”๋˜(?) BreadCrumb๋ฅผ ์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„๋กœ ๋ถ„๋ฆฌํ•˜๊ณ 
๋‚ด๋ถ€์— ์žˆ๋Š” Item ๋˜ํ•œ ์ปดํฌ๋„ŒํŠธ ๋‹จ์œ„๋กœ ๋ถ„๋ฆฌํ•˜๋‹ˆ ์™œ ์ง„์ž‘์— ์ด๋ ‡๊ฒŒ ๋ถ„๋ฆฌํ•˜์ง€ ๋ชปํ–ˆ์„๊นŒ ๋„ˆ๋ฌด ํŽธ๋ฆฌํ–ˆ๋‹ค.

๊ธฐ์กด์— Spacer๋ฅผ ์‹ค์Šตํ•˜๋ฉฐ Flex, Grid ๋˜ํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค์–ด๋ณผ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ์„ ํ–ˆ์—ˆ๋‹ค.
์˜ค๋Š˜ Flux๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉฐ ๊ถ๊ธˆ์ฆ์ด ํ’€๋ ธ๋‹ค.
์กฐ๊ธˆ ๋” ๊ณต๋ถ€ ํ•ด๋ด์•ผ๊ฒ ๋‹ค. ์•„์ง Context API๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•ด๋ณด์ง€ ์•Š์•„์„œ์ผ๊นŒ, gutter ๋ถ€๋ถ„์ด ์ดํ•ด๊ฐ€ ์ž˜ ๊ฐ€์ง€ ์•Š๋Š”๋‹ค.




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

Flux

Provider์˜ ์ดํ•ด๊ฐ€ ๋” ํ•„์š”ํ•˜๋‹ค.

๊ทผ๋ฐ ์˜๋ฌธ์ ์ด ์žˆ๋‹ค. Flux ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด๋ถ€์— Provider๋„ ํ•จ๊ป˜ ์กด์žฌํ•œ๋‹ค.

Flux Component ๋‚ด๋ถ€์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๋Š” Provider๋Š” ๊ฐ™์€ ๋””๋ ‰ํ† ๋ฆฌ์— ๋ฌถ์–ด๋„ ๋˜๋Š”๊ฑธ๊นŒ?


์•„์ง ๋””๋ ‰ํ† ๋ฆฌ ๋‚˜๋ˆ„๋Š” ๊ฒƒ์— ์ต์ˆ™ํ•˜์ง€ ์•Š๋‹ค.

์Šคํ† ๋ฆฌ ํŒŒ์ผ์ด๋‚˜ ์Šคํƒ€์ผ ํŒŒ์ผ์ด ๊ฐ™์€ ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด๋ถ€์— ์žˆ์–ด์•ผ ํ•œ๋‹ค๋Š” ์˜๊ฒฌ์ด ๋ฐ˜, ๋ณ„๋„์˜ ๋””๋ ‰ํ† ๋ฆฌ์— ์žˆ์–ด์•ผ ํ•œ๋‹ค๋Š” ์˜๊ฒฌ์ด ๋ฐ˜.

์ด๊ฑด ๋‚˜์ค‘์— ํŒ€ ๋‹จ์œ„๋กœ ํ˜‘์—…์„ ํ•  ๋•Œ ์ •ํ•˜๋Š” ์‚ฌํ•ญ๋“ค์ผ๊นŒ?



// Row.js
import styled from "@emotion/styled";
import { useMemo } from "react";
import FluxProvider from "./FluxProvider";

const AlignToCssValue = {
  top: "flex-start",
  middle: "center",
  bottom: "flex-end",
};

const StyleRow = styled.div`
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  box-sizing: border-box;

  justify-content: ${({ justify }) => justify};
  align-items: ${({ align }) => AlignToCssValue[align]};
`;

const Row = ({ children, justify, align, gutter, ...props }) => {
  const gutterStyle = useMemo(() => {
    if (Array.isArray(gutter)) {
      const horizontalGutter = gutter[0];
      const verticalGutter = gutter[1];
      return {
        marginTop: `-${verticalGutter / 2}px`,
        marginBottom: `-${verticalGutter / 2}px`,
        marginLeft: `-${horizontalGutter / 2}px`,
        marginRight: `-${horizontalGutter / 2}px`,
      };
    }
    return {
      marginLeft: `-${gutter / 2}px`,
      marginRight: `-${gutter / 2}px`,
    };
  }, [gutter]);

  return (
    <FluxProvider gutter={gutter}>
      <StyleRow
        {...props}
        align={align}
        justify={justify}
        style={{ ...props.style, ...gutterStyle }}
      >
        {children}
      </StyleRow>
    </FluxProvider>
  );
};

export default Row;

// Col.js
import styled from "@emotion/styled";
import { useMemo } from "react";
import { useFlux } from "./FluxProvider";

const StyledCol = styled.div`
  max-width: 100%fit-content;
  box-sizing: border-box;

  width: ${({ span }) => span && `${(span / 12) * 100}%`};
  margin-left: ${({ offset }) => offset && `${(offset / 12) * 100}%`};
`;

const Col = ({ children, span, offset, ...props }) => {
  const { gutter } = useFlux();
  const gutterStyle = useMemo(() => {
    if (Array.isArray(gutter)) {
      const horizontalGutter = gutter[0];
      const verticalGutter = gutter[1];
      return {
        paddingTop: `${verticalGutter / 2}px`,
        paddingBottom: `${verticalGutter / 2}px`,
        paddingLeft: `${horizontalGutter / 2}px`,
        paddingRight: `${horizontalGutter / 2}px`,
      };
    }
    return {
      paddingLeft: `${gutter / 2}px`,
      paddingRight: `${gutter / 2}px`,
    };
  }, [gutter]);

  return (
    <StyledCol
      {...props}
      span={span}
      offset={offset}
      style={{ ...props.style, ...gutterStyle }}
    >
      {children}
    </StyledCol>
  );
};

export default Col;


// FluxProvider
import { createContext, useContext } from "react";

const FluxContext = createContext();
export const useFlux = () => useContext(FluxContext);

const FluxProvider = ({ children, gutter = 0 }) => {
  return (
    <FluxContext.Provider value={{ gutter }}>{children}</FluxContext.Provider>
  );
};

export default FluxProvider;

// index.js
import Row from "./Row";
import Col from "./Col";

const Flux = {
  Row,
  Col,
};

export default Flux;

// Flux.stories.js
import Flux from "../../components/_BasicComponent/Flux";

const { Row, Col } = Flux;

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

const Box = () => {
  return (
    <div
      style={{
        backgroundColor: "#44b",
        width: "100%",
        height: 18,
        color: "white",
        textAlign: "center",
        borderRadius: 8,
      }}
    >
      Box
    </div>
  );
};

export const Default = () => {
  return (
    <Row gutter={[2, 4]}>
      <Col span={4}>
        <Box />
      </Col>
      <Col span={4}>
        <Box />
      </Col>
      <Col span={4}>
        <Box />
      </Col>
      <Col span={2}>
        <Box />
      </Col>
      <Col span={8}>
        <Box />
      </Col>
      <Col span={2}>
        <Box />
      </Col>
    </Row>
  );
};



// BreadcrumbItem.js
import styled from "@emotion/styled";
import Text from "../Text";
import Icon from "../Icon";

const BreadcrumbItemContainer = styled.div`
  display: inline-flex;
  align-items: center;
`;

const Anchor = styled.a`
  color: inherit;
  text-decoration: none;
`;

const BreadcrumbItem = ({ children, href, active, __TYPE, ...props }) => {
  return (
    <BreadcrumbItemContainer {...props}>
      <Anchor href={href}>
        <Text size={14} strong={active}>
          {children}
        </Text>
      </Anchor>
      {!active && <Icon name="chevron-right" size={22} strokeWidth={1} />}
    </BreadcrumbItemContainer>
  );
};

BreadcrumbItem.defaultProps = {
  __TYPE: "Breadcrumb.Item",
};

BreadcrumbItem.propTypes = {
  __TYPE: "Breadcrumb.Item",
};

export default BreadcrumbItem;
// index.js
import styled from "@emotion/styled";
import React from "react";
import BreadcrumbItem from "./BreadcrumbItem";

const BreadcrumbContainer = styled.nav`
  display: inline-block;
`;

const Breadcrumb = ({ children, ...props }) => {
  const items = React.Children.toArray(children)
    .filter((element) => {
      if (
        React.isValidElement(element) &&
        element.props.__TYPE === "Breadcrumb.Item"
      ) {
        return true;
      }

      console.warn("Only accepts Breadcrumb.Item as it's children.");
      return false;
    })
    .map((element, index, elements) => {
      return React.cloneElement(element, {
        ...element.props,
        active: index === elements.length - 1,
      });
    });

  return <BreadcrumbContainer>{items}</BreadcrumbContainer>;
};

Breadcrumb.Item = BreadcrumbItem;

export default Breadcrumb;
// Breadcrumb.stories.js
import Breadcrumb from "../../components/_BasicComponent/Breadcrumb";

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

export const Default = () => {
  return (
    <Breadcrumb>
      <Breadcrumb.Item>Home</Breadcrumb.Item>
      <Breadcrumb.Item>item1</Breadcrumb.Item>
      <Breadcrumb.Item>item2</Breadcrumb.Item>
      <Breadcrumb.Item>item3</Breadcrumb.Item>
    </Breadcrumb>
  );
};



Tab

// TabItem.js
import PropTypes from "prop-types";
import styled from "@emotion/styled";
import Text from "../Text";

const TabItemWrapper = styled.div`
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 140px;
  height: 60px;
  background-color: ${({ active }) => (active ? "#ddf" : "#eee")};
  cursor: pointer;
`;

const TabItem = ({ title, index, active, onCLick, ...props }) => {
  return (
    <TabItemWrapper active={active} {...props}>
      <Text strong={active}>{title}</Text>
    </TabItemWrapper>
  );
};

TabItem.defaultProps = {
  __TYPE: "Tab.Item",
};

TabItem.propTypes = {
  __TYPE: PropTypes.oneOf(["Tab.Item"]),
};

export default TabItem;
// index.js
import styled from "@emotion/styled";
import React, { useMemo, useState } from "react";
import TabItem from "./TabItem";

const childrenToArray = (children, types) => {
  return React.Children.toArray(children).filter((element) => {
    if (React.isValidElement(element) && types.includes(element.props.__TYPE)) {
      return true;
    }

    console.warn(
      `Only accepts ${Array.isArray(types) ? types.join(", ") : types}
			 as it's children.`
    );

    return false;
  });
};

const TabItemContainer = styled.div`
  border: 2px solid #ddd;
  background-color: #eee;
`;

const Tab = ({ children, active, ...props }) => {
  const [currentActive, setCurrentActive] = useState(() => {
    if (active) return active;
    else {
      const index = childrenToArray(children, "Tab.Item")[0].props.index;
      return index;
    }
  });

  const items = useMemo(() => {
    return childrenToArray(children, "Tab.Item").map((element) => {
      return React.cloneElement(element, {
        ...element.props,
        key: element.props.index,
        active: element.props.index === currentActive,
        onClick: () => {
          setCurrentActive(element.props.index);
        },
      });
    });
  }, [children, currentActive]);

  const activeItem = useMemo(
    () => items.find((element) => currentActive === element.props.index),
    [currentActive, items]
  );

  return (
    <div>
      <TabItemContainer>{items}</TabItemContainer>
      <div>{activeItem.props.children}</div>
    </div>
  );
};

Tab.Item = TabItem;

export default Tab;




[2] ์‚ฌ์šฉ์ž ์ •์˜ Hook ์‹ค์Šต

์‚ฌ์šฉ์ž ์ •์˜ ํ›…๋„ storybook์—์„œ ๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์˜ค๋Š˜ ์ฒ˜์Œ ์•Œ์•˜๋‹ค.

์Šคํ† ๋ฆฌ๋ถ์€,, ์ปดํฌ๋„ŒํŠธ์˜ ๊ตฌ์กฐ, ๋‹จ์œ„๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์•ฝ๊ฐ„์˜ ๊ณ ์ •๊ด€๋…์ด ์žˆ์—ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค.



useHover

// useHover.js
import { useCallback, useEffect, useState, useRef } 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: tomato;
`;

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

  return (
    <>
      <Box ref={ref} />
      {hover ? <div>HOVER</div> : ""}
    </>
  );
};



useScroll

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

    element.addEventListener("scroll", handleScroll, { passive: true });
    return () => {
      element.removeEventListener("scroll", handleScroll);
    };
  }, [ref]);

  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 />
      </Box>
      <button
        onClick={() => {
          ref.current.scrollTo({ top: 20000, left: 20000, behavior: "smooth" });
        }}
      >
        Scroll
      </button>
      {coord.x}, {coord.y}
    </>
  );
};



useKey, useKeyPress


  • useKey
// 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 keydown Event!");
  });

  useKey("keyup", "q", () => {
    alert("q keyup Event!");
  });
  return <>useKey</>;
};


  • useKeyPress
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);
    };
  }, [handleKeyDown, handleKeyUp]);

  return keyPressed;
};

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

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

export const Default = () => {
  const pressed = useKeyPress("?");
  return <>{pressed ? "Peeo-A-Bool" : "Press ? Key"}</>;
};



useClickAway

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

const events = ["mousedown", "touchstart"];

const useClickAway = (handler) => {
  const ref = useRef(null);
  const savedHandler = useRef(handler);
  // ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด useRef๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  // ์ด๋ฒคํŠธ๊ฐ€ ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ ์ „์ฒด๊ฐ€ ์žฌ๋ Œ๋”๋ง ๋˜๋Š” ์„ฑ๋Šฅ ์ €ํ•˜ ์š”์ธ์„ ํ•ด๊ฒฐํ•œ๋‹ค.

  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 { useState } from "react";
import styled from "@emotion/styled";
import useClickAway from "../../hooks/useClickAway";

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

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

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
      </Popover>
    </div>
  );
};





์ถœ์ฒ˜

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

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

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

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