[DAY-53] React (6)
๐ ํท๊ฐ๋ ธ๋ & ๋ชฐ๋๋ ๋ถ๋ถ๋ค๋ง ์ ๋ฆฌํ๋ ๋๋ง์ 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>
);
};
Breadcrumb
// 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>
);
};
๋๊ธ๋จ๊ธฐ๊ธฐ