023

๐Ÿ˜‰ ํ—ท๊ฐˆ๋ ธ๋˜ & ๋ชฐ๋ž๋˜ ๋ถ€๋ถ„๋“ค๋งŒ ์ •๋ฆฌํ•˜๋Š” ๋‚˜๋งŒ์˜ 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} />;
};





์ถœ์ฒ˜

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

https://ko.reactjs.org/docs/context.html

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

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

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