[DAY-52] React (5)
๐ ํท๊ฐ๋ ธ๋ & ๋ชฐ๋๋ ๋ถ๋ถ๋ค๋ง ์ ๋ฆฌํ๋ ๋๋ง์ TIL
๐ฏ ๋ชจ๋ ๊ฐ์ ๋ด์ฉ์ ์ ์ง ์์์!
์ค๋์ ์๊ฐ์?
์ค์ต ์งํํ๋ค๊ฐ ์ฃฝ์ ๋ป ํ์ต๋๋ค..
์คํ ์ก์์ฃผ๋ extension ์ฐพ์๋ด์ผ๊ฒ ์ด์.
ํ๊ตฌํ ๋ ์คํ ๋๋ฌธ์ ํ์๊ฐ ์ฉ ๋ ๋ฆฌ๋ ์ฌ๋ ๋๊ตฌ๋ผ๊ณ ? ๋ค ์ ๋๋ค.
์ค์ผ๋ ํค ์ปดํฌ๋ํธ๊ฐ ๊ฐ์ฅ ์ ๊ธฐํ๋๋ฐ์.
์ค์ผ๋ ํค ์ปดํฌ๋ํธ๊ฐ ๋จผ์ ๋ ๋๋ง ๋ ํ, loading์ด ๋๋๋ฉด ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ก ๊ต์ฒดํด์ฃผ๋ ๊ฑธ๊น์?
ํ๋ก์ ํธ์์ ํ์ฉํด๋ณด๊ณ ์ถ์ด์..!
[1] Component ์ค์ต
ํ์.. ์ ๋ง ๋๋ ค์?
์ ๋ ๋ง์ฐ์ค ์์ฐ๊ณ ํ์ ์น๊ณ ์ถ์ด์ ์์
Avatar
// index.js
import styled from "@emotion/styled";
import { useEffect, useState } from "react";
import ImageComponent from "../Image";
import AvatarGroup from "./AvatarGroup";
const ShapeToCssValue = {
circle: "50%",
round: "4px",
square: "0px",
};
const AvatarWrapper = styled.div`
position: relative;
display: inline-block;
border: 1px solid #dadada;
border-radius: ${({ shape }) => ShapeToCssValue[shape]};
background-color: #eee;
overflow: hidden;
> img {
transition: opacity 0.2s ease-out;
}
`;
const Avatar = ({
lazy,
threshold,
src,
size = 70,
shape = "circle", // round, square
placeholder,
alt,
mode = "cover",
__TYPE,
...props
}) => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const image = new Image();
image.src = src;
image.onload = () => setLoaded(true);
}, [src]);
return (
<AvatarWrapper {...props} shape={shape}>
<ImageComponent
block
lazy={lazy}
threshold={threshold}
width={size}
height={size}
src={src}
placeholder={placeholder}
alt={alt}
mode={mode}
style={{ opacity: loaded ? 1 : 0 }}
/>
</AvatarWrapper>
);
};
Avatar.defaultProps = {
__TYPE: "Avatar",
};
Avatar.propTypes = {
__TYPE: "Avatar",
};
Avatar.Group = AvatarGroup;
export default Avatar;
// AvatarGroup,js
import React from "react";
const AvatarGroup = ({ children, shape = "circle", size = 70, ...props }) => {
const avatars = React.Children.toArray(children)
.filter((element) => {
return !!React.isValidElement(element);
})
.map((avatar, index, avatars) => {
return React.cloneElement(avatar, {
...avatar.props,
size,
shape,
style: {
...avatar.props.style,
marginLeft: -size / 5,
zIndex: avatars.length - index,
},
});
});
return (
<div style={{ paddingLeft: size / 5 }} {...props}>
{avatars}
</div>
);
};
export default AvatarGroup;
// Avatar.stories.js
import Avatar from "../../components/_BasicComponent/Avatar";
export default {
title: "Component/Avatar",
component: Avatar,
argTypes: {
src: { defaultValue: "https://picsum.photos/200" },
shape: {
defaultValue: "circle",
control: "inline-radio",
options: ["circle", "round", "sqaure"],
},
size: {
defaultValue: 70,
control: { type: "range", min: 40, max: 200 },
},
mode: {
defaultValue: "cover",
control: "inline-radio",
options: ["contain", "cover", "fill"],
},
},
};
export const Default = (args) => <Avatar {...args} />;
export const Group = () => {
return (
<Avatar.Group size={40}>
<Avatar src="https://picsum.photos/200?1" />
<Avatar src="https://picsum.photos/200?2" />
<Avatar src="https://picsum.photos/200?3" />
<Avatar src="https://picsum.photos/200?4" />
</Avatar.Group>
);
};
Slider
// index.js
import { useState, useCallback, useEffect, useRef } from "react";
import styled from "@emotion/styled";
const SliderContainer = styled.div`
position: relative;
width: 100%;
height: 16px;
`;
const Handle = styled.div`
position: absolute;
top: 8px;
left: 0;
width: 12px;
height: 12px;
transform: translate(-50%, -50%);
border: 2px solid #44b;
border-radius: 50%;
background-color: white;
cursor: grab;
`;
const Track = styled.div`
position: absolute;
top: 6px;
left: 0;
width: 0;
height: 4px;
border-radius: 2px;
background-color: #44b;
`;
const Rail = styled.div`
position: absolute;
top: 6px;
left: 0;
width: 100%;
height: 4px;
border-radius: 2px;
background-color: #aaa;
`;
const Slider = ({ min, max, step = 0.1, defaultValue, onChange, ...props }) => {
const sliderRef = useRef(null);
const [dragging, setDragging] = useState(false);
const [value, setValue] = useState(defaultValue ? defaultValue : min);
const handleMouseDown = useCallback(() => {
setDragging(true);
}, []);
const handleMouseUp = useCallback(() => {
setDragging(false);
}, []);
useEffect(() => {
const handleMouseMove = (e) => {
if (!dragging) return;
const handleOffset = e.pageX - sliderRef.current.offsetLeft;
const sliderWidth = sliderRef.current.offsetWidth;
const track = handleOffset / sliderWidth;
let newValue;
if (track < 0) {
newValue = min;
} else if (track > 1) {
newValue = max;
} else {
newValue = Math.round((min + (max - min) * track) / step) * step;
newValue = Math.min(max, Math.max(min, newValue));
}
setValue(newValue);
onChange && onChange(newValue);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [value, min, max, dragging, sliderRef, handleMouseUp, onChange, step]);
const percentage = ((value - min) / (max - min)) * 100;
return (
<SliderContainer ref={sliderRef} {...props}>
<Rail />
<Track style={{ width: `${percentage}%` }} />
<Handle
onMouseDown={handleMouseDown}
style={{ left: `${percentage}%` }}
/>
</SliderContainer>
);
};
export default Slider;
// Slider.stories.js
import Slider from "../../components/_BasicComponent/Slider";
import Spacer from "../../components/_BasicComponent/Spacer";
import Icon from "../../components/_BasicComponent/Icon";
export default {
title: "Component/Slider",
component: Slider,
argTypes: {
defaultValue: { defaultValue: 1, control: "number" },
min: { defaultValue: 1, control: "number" },
max: { defaultValue: 100, control: "number" },
step: { defaultValue: 0.1, control: "number" },
onChange: { action: "onChange" },
},
};
export const Default = (args) => <Slider {...args} />;
export const VolumeControl = (args) => {
return (
<Spacer>
<Icon name="volume" />
<Slider style={{ width: 100, display: "inline-block" }} {...args} />
<Icon name="volume-2" />
</Spacer>
);
};
Progress
// index.js
import styled from "@emotion/styled";
const ProgressContainer = styled.div`
position: relative;
width: 100%;
height: 16px;
`;
const Track = styled.div`
position: absolute;
top: 6px;
left: 0;
width: 0;
height: 4px;
border-radius: 2px;
background-color: #44b;
background-size: 20px 20px;
background-image: linear-gradient(
45deg,
rgba(255, 255, 255, 0.15) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.15) 75%,
transparent 75%,
transparent 100%
);
animation: move 1000ms linear infinite;
transition: width 100ms linear;
@keyframes move {
from {
background-position: 0 0;
}
to {
background-position: 40px 0;
}
}
`;
const Rail = styled.div`
position: absolute;
top: 6px;
left: 0;
width: 100%;
height: 4px;
border-radius: 2px;
background-color: #aaa;
`;
const Progress = ({ value, ...props }) => {
return (
<ProgressContainer {...props}>
<Rail />
<Track style={{ width: `${value}%` }} />
</ProgressContainer>
);
};
export default Progress;
// Progress.stories.js
import { useState } from "react";
import Progress from "../../components/_BasicComponent/Progress";
export default {
title: "Component/Progress",
component: Progress,
argTypes: {
value: { defaultValue: 10, control: "number" },
},
};
export const Default = (args) => {
const [value, setValue] = useState(20);
return (
<div>
<button onClick={() => setValue(100)}>Change Value</button>
<Progress {...args} value={value} />
</div>
);
};
Divider
// index.js
import styled from "@emotion/styled";
const Line = styled.hr`
border: none;
background-color: #aaa;
&.vertical {
position: relative;
top: -1;
display: inline-block;
width: 1px;
height: 13px;
vertical-align: middle;
}
&.horizontal {
display: block;
width: 100%;
height: 1px;
}
`;
const Divider = ({ type = "horizontal", size = 8, ...props }) => {
const dividerStyle = {
margin: type === "vertical" ? `0 ${size}px` : `${size}px 0`,
};
return (
<Line
{...props}
className={type}
style={{ ...dividerStyle, ...props.style }}
/>
);
};
export default Divider;
// Divider.stories.js
import Divider from "../../components/_BasicComponent/Divider";
import Text from "../../components/_BasicComponent/Text";
export default {
title: "Component/Divider",
component: Divider,
};
export const Horizontal = () => {
return (
<>
<Text>์</Text>
<Divider type="horizontal" />
<Text>์๋</Text>
</>
);
};
export const Vertical = () => {
return (
<>
<Text>์</Text>
<Divider type="vertical" />
<Text>์๋</Text>
</>
);
};
Skeleton
Skeleton์ด ๋ฌด์์ธ๊ณ ํ๋ ๋ฐ๋ก ์ด๊ฒ์ด์๋ค.
๋ฌด์ธ๊ฐ ์๋ค๋ ๊ฒ์ ์ฌ์ฉ์์๊ฒ ๋ฏธ๋ฆฌ ์๋ ค์ฃผ๊ธฐ ์ํด ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ์๋ค..! ๐ฎ
// index.js
import Box from "./Box";
import Circle from "./Circle";
import Paragraph from "./Paragraph";
const Skeleton = {
Box,
Circle,
Paragraph,
};
export default Skeleton;
// Base.js
import styled from "@emotion/styled";
const Base = styled.div`
display: inline-block;
border-radius: 4px;
background-image: linear-gradient(
90deg,
#dfe3e8 0px,
#efefef 40px,
#dfe3e8 80px
);
background-size: 200% 100%;
background-position: 0 center;
animation: skeleton--zoom-in 0.2s ease-out, skeleton--loading 2s infinite
linear;
@keyframes skeleton--zoom-in {
0% {
opacity: 0;
transform: scale(0.95);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes skeleton--loading {
0% {
background-position-x: 100%;
}
50% {
background-position-x: -100%;
}
100% {
background-position-x: -100%;
}
}
`;
export default Base;
// Circle.js
import styled from "@emotion/styled";
import Base from "./Base";
const CircularBase = styled(Base)`
border-radius: 50%;
`;
const Circle = ({ size }) => (
<CircularBase style={{ width: size, height: size }} />
);
export default Circle;
// Box.js
import Base from "./Base";
const Box = ({ width, height }) => <Base style={{ width, height }} />;
export default Box;
// Paragraph.js
import Box from "./Box";
const Paragraph = ({ line = 3, ...props }) => {
return (
<div {...props}>
{Array.from(Array(line), (_, index) =>
index !== line - 1 ? (
<Box width="100%" height={16} key={index} />
) : (
<Box width="64%" height={16} key={index} />
)
)}
</div>
);
};
export default Paragraph;
// Skeleton.stories.js
import Skeleton from "../../components/_BasicComponent/Skeleton";
export default {
title: "Component/Skeleton",
};
export const Box = (args) => <Skeleton.Box {...args} />;
Box.argTypes = {
width: { defaultValue: 200, control: "number" },
height: { defaultValue: 100, control: "number" },
};
export const Circle = (args) => <Skeleton.Circle {...args} />;
Circle.argTypes = {
size: { defaultValue: 100, control: "number" },
};
export const Paragraph = (args) => <Skeleton.Paragraph {...args} />;
Paragraph.argTypes = {
line: { defaultValue: 3, control: "number" },
};
export const Sample = () => {
return (
<>
<div>
<div style={{ float: "left", marginRight: 16 }}>
<Skeleton.Circle size={60} />
</div>
<div style={{ float: "left", width: "80%" }}>
<Skeleton.Paragraph line={4} />
</div>
<div style={{ clear: "both" }} />
</div>
</>
);
};
Input
// index.js
import styled from "@emotion/styled";
const Wrapper = styled.div`
display: ${({ block }) => (block ? "block" : "inline-block")};
`;
const Label = styled.label`
display: block;
font-size: 12px;
`;
const StyledInput = styled.input`
width: 100%;
padding: 4px 8px;
border: 1px solid ${({ invalid }) => (invalid ? "red" : "gray")};
border-radius: 4px;
box-sizing: border-box;
`;
const Input = ({
label,
block = false,
invalid = false,
required = false,
disabled = false,
readonly = false,
wrapperProps,
...props
}) => {
return (
<Wrapper block={block} {...wrapperProps}>
<Label>{label}</Label>
<StyledInput
invalid={invalid}
required={required}
disabled={disabled}
readOnly={readonly}
{...props}
/>
</Wrapper>
);
};
export default Input;
// Input.stories.js
import Input from "../../components/_BasicComponent/Input";
export default {
title: "Component/Input",
component: Input,
argTypes: {
label: {
defaultValue: "Label",
control: "text",
},
block: {
defaultValue: false,
control: "boolean",
},
invalid: {
defaultValue: false,
control: "boolean",
},
required: {
defaultValue: false,
control: "boolean",
},
disabled: {
defaultValue: false,
control: "boolean",
},
readonly: {
defaultValue: false,
control: "boolean",
},
},
};
export const Default = (args) => <Input {...args} />;
Select
// index.js
import styled from "@emotion/styled";
const Wrapper = styled.div`
display: ${({ block }) => (block ? "block" : "inline-block")};
`;
const Label = styled.label`
display: block;
font-size: 12px;
`;
const StyledSelect = styled.select`
width: 100%;
padding: 4px 8px;
border: 1px solid ${({ invalid }) => (invalid ? "red" : "gray")}
border-radius: 4px;
box-sizing: border-box;
`;
const Select = ({
data,
label,
placeholder,
block = false,
invalid = false,
required = false,
disabled = false,
wrapperaProps,
...props
}) => {
const formattedDate = data.map((item) =>
typeof item === "string" ? { label: item, value: item } : item
);
const options = formattedDate.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
));
if (placeholder) {
options.unshift(
<option key="placeholder" value="" hidden>
{placeholder}
</option>
);
}
return (
<Wrapper block={block} {...wrapperaProps}>
<Label>{label}</Label>
<StyledSelect
invalid={invalid}
required={required}
disabled={disabled}
{...props}
>
{options}
</StyledSelect>
</Wrapper>
);
};
export default Select;
// Select.stories.js
import Select from "../../components/_BasicComponent/Select";
export default {
title: "Component/Select",
component: Select,
argTypes: {
label: {
defaultValue: "Label",
control: "text",
},
placeholder: {
defaultValue: "Placeholder",
control: "text",
},
block: {
defaultValue: false,
control: "boolean",
},
invalid: {
defaultValue: false,
control: "boolean",
},
disabled: {
defaultValue: false,
control: "boolean",
},
required: {
defaultValue: false,
control: "boolean",
},
},
};
export const Default = (args) => (
<Select
data={["Item 1", "Item 2", { label: "Item 3", value: "value" }]}
{...args}
/>
);
๋๊ธ๋จ๊ธฐ๊ธฐ