[DAY-51] React (4)
๐ ํท๊ฐ๋ ธ๋ & ๋ชฐ๋๋ ๋ถ๋ถ๋ค๋ง ์ ๋ฆฌํ๋ ๋๋ง์ TIL
๐ฏ ๋ชจ๋ ๊ฐ์ ๋ด์ฉ์ ์ ์ง ์์์!
์ค๋์ ์๊ฐ์?
Vuex์ ๋น์ทํ Context API..
์ด ์น๊ตฌ๋ ํท๊ฐ๋ฆฌ๋๋ฐ Redux ์ด๋กํ์ง..? ํคํค
[1] Context API
Component
๋ ํธ๋ฆฌ ๊ตฌ์กฐ๋ก ์ด๋ฃจ์ด์ ธ ์์ต๋๋ค.
๊ฐ๋ฐ์ ์งํํ๋ค๋ณด๋ฉด ํธ๋ฆฌ ๋ ๋ฒจ์ด ๊น์ด์ง๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค.
๋ฐ๋ผ์ props
๋ฅผ ๋๊ฒจ์ค ๋ ํธ๋ฆฌ๋ฅผ ๊ณ์ ํ๊ณ ๋์ด๊ฐ์ผ ํ๋ Prop Drilling
ํ์์ด ๋ฐ์ํฉ๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ๋๊ตฌ ์ค ํ๋๊ฐ Context API
์
๋๋ค. (์ ์ญ ์ํ ๊ด๋ฆฌ)
Context Provider
๋ก ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ๊ณ , Context Consumer
๋ก ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ฌ์ฉํฉ๋๋ค.
import { createContext, useContext } from "react";
const TaskContext = createContext();
export const useTasks = () => useContext(TaskContext);
ํด๋น Context๋ฅผ React.createContext๋ก ๋ง๋ค์ด ์ฌ์ฉํฉ๋๋ค.
const TaskProvider = ({ children }) => {
const [tasks, setTasks] = useLocalStorage("TASKS", []);
const addTask = (content) => {
setTasks([
...tasks,
{
id: v4(),
content,
complete: false,
},
]);
};
const updateTask = (id, status) => {
setTasks(
tasks.map((item) =>
item.id === id ? { ...item, complete: status } : item
)
);
};
const removeTask = (id) => {
setTasks(tasks.filter((item) => item.id !== id));
};
return (
<TaskContext.Provider value={{ tasks, addTask, updateTask, removeTask }}>
{children}
</TaskContext.Provider>
// Context์ ๋ณํ๋ฅผ ์๋ฆฌ๋ ์ญํ ์ ํฉ๋๋ค.
// value prop์ ๋ฐ์์ ์ด ๊ฐ์ ํ์์ ์๋ ์ปดํฌ๋ํธ์๊ฒ ์ ๋ฌํฉ๋๋ค.
);
};
export default TaskProvider;
์์ฑํ TaskContext
์์ ์ฌ์ฉํ Provider
์ ๋ด์ฉ์ Component
ํ์์ผ๋ก return
ํด ์ฌ์ฉํฉ๋๋ค.
// Task.js ์ผ๋ถ
const { updateTask, removeTask } = useTasks();
ํจ์ ๋๋ ๊ฐ์ ๊บผ๋ด์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๋ํ ๋ค์๊ณผ ๊ฐ์ ํํ๋ก context ๊ฐ์ ์ง์ผ๋ณด๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
<TaskContext.Consumer>{value}</TaskContext.Consumer>
[1-1] ํน์ง
Context
์ ์ฃผ๋ ์ฉ๋๋ ๋ค์ํ ๋ ๋ฒจ์ ๋ค์คํ
๋ ๋ง์ ์ปดํฌ๋ํธ๋ค์๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ๊ฒ ์
๋๋ค.
๋ฐ๋ผ์ ํด๋น ์ฉ๋๊ฐ ๊ผญ ํ์ํ ๋๋ง Context
๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
์๋ํ๋ฉด Context
๋ฅผ ์ฌ์ฉํ๋ฉด ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฌ์ฉํ๊ธฐ๊ฐ ์ด๋ ค์์ง๋๋ค.
Context consumer
๋ก ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ ๋ชจ๋ ์ปดํฌ๋ํธ๋ค์, Provider
๋ก ์กฐ์๋๋ ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ ์ ๋ถ ์ฌ๋ ๋๋ง ๋๊ธฐ ๋๋ฌธ์
๋๋ค.
์ด ๊ฒฝ์ฐ, Provider
์ ๋ถ๋ชจ๊ฐ ๋ ๋๋ง ๋ ๋๋ง๋ค ๋ถํ์ํ๊ฒ ํ์ ์ปดํฌ๋ํธ๋ค์ด ๋ค์ ๋ ๋๋ง๋ฉ๋๋ค.
[2] ์ปดํฌ๋ํธ ์ค์ต
- Upload
// index.js
import { useRef, useState } from "react";
import styled from "@emotion/styled";
const UploadContainer = styled.div`
display: inline-block;
cursor: pointer;
`;
const Input = styled.input`
display: none;
`;
const Upload = ({
children,
droppable,
name,
accept,
value,
onChange,
...props
}) => {
const [file, setFile] = useState(value);
const [dragging, setDragging] = useState(false);
const inputRef = useRef(null);
const handleFileChange = (e) => {
const files = e.target.files;
const changedFile = files[0];
setFile(changedFile);
onChange && onChange(changedFile);
};
const handleChooseFile = () => {
inputRef.current.click();
};
const handleDragEnter = (e) => {
if (!droppable) return;
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
setDragging(true);
}
};
const handleDragLeave = (e) => {
if (!droppable) return;
e.preventDefault();
e.stopPropagation();
setDragging(false);
};
const handleDragOver = (e) => {
if (!droppable) return;
e.preventDefault();
e.stopPropagation();
};
const handleFileDrop = (e) => {
if (!droppable) return;
e.preventDefault();
e.stopPropagation();
const files = e.dataTransfer.files;
const changedFile = files[0];
setFile(changedFile);
onChange && onChange(changedFile);
setDragging(false);
};
return (
<UploadContainer
onClick={handleChooseFile}
onDrop={handleFileDrop}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
{...props}
>
<Input
ref={inputRef}
type="file"
name={name}
accept={accept}
onChange={handleFileChange}
/>
{typeof children === "function" ? children(file, dragging) : children}
</UploadContainer>
);
};
export default Upload;
// Upload.stories.js
import Upload from ".";
export default {
title: "Component/Upload",
component: Upload,
};
export const Default = () => {
return (
<Upload>
<button>Upload!</button>
</Upload>
);
};
export const AccessFile = () => {
return (
<Upload>
{(file) => <button>{file ? file.name : "CLICK ME"}</button>}
</Upload>
);
};
export const Droppable = () => {
return (
<Upload droppable>
{(file, dragging) => (
<div
style={{
width: 300,
height: 100,
border: "4px dashed #aaa",
borderColor: dragging ? "black" : "#aaa",
}}
>
{file ? file.name : "Click or drag file to this area to upload"}
</div>
)}
</Upload>
);
};
- Badge
// index.js
import styled from "@emotion/styled";
const BadgeContainer = styled.div`
position: relative;
display: inline-block;
`;
const Super = styled.sup`
position: absolute;
top: 0;
right: 0;
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 8px;
font-size: 12px;
border-radius: 20px;
color: white;
background-color: tomato;
transform: translate(50%, -50%);
&.dot {
padding: 0;
width: 6px;
height: 6px;
border-radius: 50%;
}
`;
const Bedge = ({
children,
count,
maxCount,
showZero,
dot = false,
backgroundColor,
textColor,
...props
}) => {
const colorStyle = {
backgroundColor,
color: textColor,
};
let badge = null;
if (count) {
badge = (
<Super style={colorStyle}>
{maxCount && count > maxCount ? `${maxCount}+` : count}
</Super>
);
} else {
if (count !== undefined) {
badge = showZero ? <Super style={colorStyle}>0</Super> : null;
} else if (dot) {
badge = <Super style={colorStyle} className="dot"></Super>;
}
}
return (
<BadgeContainer {...props}>
{children}
{badge}
</BadgeContainer>
);
};
export default Bedge;
// Badge.stories.js
import Bedge from ".";
import Image from "../Image";
export default {
title: "Component/Bedge",
component: Bedge,
argTypes: {
count: {
defaultValue: 10,
control: "number",
},
maxCount: {
defaultValue: 100,
control: "number",
},
backgroundColor: { control: "color" },
textColor: { control: "color" },
showZero: {
defaultValue: false,
control: "boolean",
},
},
};
export const Default = (args) => {
return (
<Bedge {...args}>
<Image
src="https://picsum.photos/60"
width={60}
style={{ borderRadius: 0 }}
/>
</Bedge>
);
};
export const Dot = () => {
return (
<Bedge dot>
<Image
src="https://picsum.photos/60"
width={40}
style={{ borderRadius: 0 }}
/>
</Bedge>
);
};
- Icon
// index.js
import styled from "@emotion/styled";
import { Buffer } from "buffer";
const IconWrapper = styled.i`
display: inline-block;
`;
const Icon = ({
name,
size = 16,
strokeWidth = 2,
rotate,
color = "#222",
...props
}) => {
const shapeStyle = {
width: size,
height: size,
transform: rotate ? `rotate(${rotate}deg)` : undefined,
};
const iconStyle = {
"stroke-width": strokeWidth,
stroke: color,
width: size,
height: size,
};
const icon = require("feather-icons").icons[name];
const svg = icon ? icon.toSvg(iconStyle) : "";
const base64 = Buffer.from(svg, "utf8").toString("base64");
return (
<IconWrapper {...props} style={shapeStyle}>
<img src={`data:image/svg+xml;base64,${base64}`} alt={name} />
</IconWrapper>
);
};
export default Icon;
import Icon from ".";
export default {
title: "Component/Icon",
component: Icon,
argTypes: {
name: { defaultValue: "box", control: "text" },
size: { defaultValue: 16, control: { type: "range", min: 16, max: 80 } },
strokeWidth: {
defaultValue: 2,
control: { type: "range", min: 2, max: 6 },
},
color: {
defaultValue: "#222",
control: "color",
},
rotate: { defaultValue: 0, control: { type: "range", min: 0, max: 360 } },
},
};
export const Default = (args) => {
return <Icon {...args} />;
};
๋๊ธ๋จ๊ธฐ๊ธฐ