๊ด€๋ฆฌ ๋ฉ”๋‰ด

Daily Front_Minhhk

[React] React Custom Component ๋ณธ๋ฌธ

Code๊ฐœ๋ฐœ์ผ์ง€

[React] React Custom Component

Minhhk 2022. 12. 26. 20:49
๐Ÿ“Œ ์ ์  ๋‚œ์ด๋„๊ฐ€ ์–ด๋ ค์›Œ์ง€๊ณ  ์žˆ๋‹ค,,, ์ž˜ ํ•˜๊ณ  ์žˆ๋Š”๊ฑด์ง€ ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ!
๐Ÿ“Œ styled-components์™€ ์†์„ฑ์„ ์‚ผํ•ญ์—ฐ์‚ฐ์ž๋กœ ๋‚˜ํƒ€๋‚ด์—ˆ๋‹ค

 

Bare minimum Requirement

 

modal.js

>

import { useState } from "react";
import styled from "styled-components";

export const ModalContainer = styled.div`
  // TODO : Modal์„ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ ์ „์ฒด์ ์œผ๋กœ ํ•„์š”ํ•œ CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
`;

export const ModalBackdrop = styled.div`
  // TODO : Modal์ด ๋–ด์„ ๋•Œ์˜ ๋ฐฐ๊ฒฝ์„ ๊น”์•„์ฃผ๋Š” CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  background-color: rgba(0, 0, 0, 0.3);
  z-index: 333;
  display: flex;
  justify-content: center;
  align-items: center;

  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
`;

export const ModalBtn = styled.button`
  background-color: var(--coz-purple-600);
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
  cursor: grab;
`;

export const ModalView = styled.div.attrs((props) => ({
  // attrs ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด์„œ ์•„๋ž˜์™€ ๊ฐ™์ด div ์—˜๋ฆฌ๋จผํŠธ์— ์†์„ฑ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  role: "dialog",
}))`
  // TODO : Modal์ฐฝ CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  width: 500px;
  height: 300px;
  background: white;
  display: flex;
  flex-direction: column;
  align-items: center;
  border-radius: 10px;
  > div.text {
    font-size: 30px;
    margin: 20px;
    color: violet;
  }
`;
// Exitbtn ์ƒ์„ฑ -> css
export const Exitbtn = styled.button`
  background-color: white;
  color: black;
  padding: 10px 15px;
  font-size: 15px;
  margin: 10px;
`;

export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  const openModalHandler = () => {
    // TODO : isOpen์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
    setIsOpen(!isOpen);
  };

  return (
    <>
      <ModalContainer onClick={openModalHandler}>
        <ModalBtn
          // TODO : ํด๋ฆญํ•˜๋ฉด Modal์ด ์—ด๋ฆฐ ์ƒํƒœ(isOpen)๋ฅผ boolean ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
          onClick={openModalHandler}
        >
          {/* TODO : ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ํ™œ์šฉํ•ด์„œ Modal์ด ์—ด๋ฆฐ ์ƒํƒœ(isOpen์ด true์ธ ์ƒํƒœ)์ผ ๋•Œ๋Š” ModalBtn์˜ ๋‚ด๋ถ€ ํ…์ŠคํŠธ๊ฐ€ 'Opened!' ๋กœ Modal์ด ๋‹ซํžŒ ์ƒํƒœ(isOpen์ด false์ธ ์ƒํƒœ)์ผ ๋•Œ๋Š” ModalBtn ์˜ ๋‚ด๋ถ€ ํ…์ŠคํŠธ๊ฐ€ 'Open Modal'์ด ๋˜๋„๋ก ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. */}
          {isOpen ? "Opened!" : "Open Modal"}
        </ModalBtn>
        {/* TODO : ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ํ™œ์šฉํ•ด์„œ Modal์ด ์—ด๋ฆฐ ์ƒํƒœ(isOpen์ด true์ธ ์ƒํƒœ)์ผ ๋•Œ๋งŒ ๋ชจ๋‹ฌ์ฐฝ๊ณผ ๋ฐฐ๊ฒฝ์ด ๋œฐ ์ˆ˜ ์žˆ๊ฒŒ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. */}
        {isOpen === false ? null : (
          <ModalBackdrop>
            {/* ๋ชจ๋‹ฌ์ฐฝ์„ ํด๋ฆญ ํ•ด๋„ ์ฐฝ์ด ๊บผ์ง€์ง€ ์•Š์Œ,, e => e.stopPropagation()*/}
            <ModalView onClick={(e) => e.stopPropagation()}>
              <Exitbtn onClick={openModalHandler}>X</Exitbtn>
              <div className="text">Hello CodeStates</div>
            </ModalView>
          </ModalBackdrop>
        )}
      </ModalContainer>
    </>
  );
};

<ModalBackdrop>๊ณผ <Exitbtn>์„ ํด๋ฆญํ•˜๋ฉด 

<ModalView>๊ฐ€ ๋‚˜ํƒ€๋‚œ๋‹ค.

 

<ModalView>๋ฅผ ๋‹ซ์„ ๋•Œ <ModalBackdrop>๊ณผ <Exitbtn>์„ ํด๋ฆญํ•ด์•ผ ๋‹ซํ˜€์•ผ ํ•˜๋Š”๋ฐ

<ModalView>๋ฅผ ๋ˆŒ๋Ÿฌ๋„ ๋‹ซํžŒ๋‹ค.

 

๋ถ€๋ชจ ์—˜๋ฆฌ๋จผํŠธ์—๊ฒŒ๋„ ์ด๋ฒคํŠธ๊ฐ€ ์ „ํŒŒ๋˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด e.stopPropagation์„ ์‚ฌ์šฉํ•œ๋‹ค.

 

e.preventDefault๋Š” ๊ณ ์œ  ๋™์ž‘์„ ์ค‘๋‹จ์‹œํ‚ค๊ณ ,

e.stopPropagation๋Š” ์ƒ์œ„ ์—˜๋ฆฌ๋จผํŠธ๋“ค๋กœ์˜ ์ด๋ฒคํŠธ ์ „ํŒŒ๋ฅผ ์ค‘๋‹จ์‹œํ‚จ๋‹ค.

๐Ÿ”ฅ e.stopPropagation()

 

e.preventDefault()

html ์—์„œ a ํƒœ๊ทธ๋‚˜ submit ํƒœ๊ทธ๋Š” ๊ณ ์œ ์˜ ๋™์ž‘์ด ์žˆ๋‹ค.

ํŽ˜์ด์ง€๋ฅผ ์ด๋™์‹œํ‚จ๋‹ค๊ฑฐ๋‚˜ form ์•ˆ์— ์žˆ๋Š” input ๋“ฑ์„ ์ „์†กํ•œ๋‹ค๋˜๊ฐ€ ๊ทธ๋Ÿฌํ•œ ๋™์ž‘์ด ์žˆ๋Š”๋ฐ 

e.preventDefault ๋Š” ๊ทธ ๋™์ž‘์„ ์ค‘๋‹จ์‹œํ‚จ๋‹ค.

 

 

 

Toggle.js

>

import { useState } from "react";
import styled from "styled-components";

const ToggleContainer = styled.div`
  position: relative;
  margin-top: 8rem;
  left: 47%;
  cursor: pointer;

  > .toggle-container {
    width: 50px;
    height: 24px;
    border-radius: 30px;
    background-color: #8b8b8b;
    // TODO : .toggle--checked ํด๋ž˜์Šค๊ฐ€ ํ™œ์„ฑํ™” ๋˜์—ˆ์„ ๊ฒฝ์šฐ์˜ CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  }
  > .toggle--checked {
    background-color: purple;
  }

  > .toggle-circle {
    position: absolute;
    top: 1px;
    left: 1px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background-color: #ffffff;
    transition: 0.5s;
    // TODO : .toggle--checked ํด๋ž˜์Šค๊ฐ€ ํ™œ์„ฑํ™” ๋˜์—ˆ์„ ๊ฒฝ์šฐ์˜ CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  }
  > .toggle--checked {
    left: 27px;
    transition: 0.5s;
  }
`;

const Desc = styled.div`
  // TODO : ์„ค๋ช… ๋ถ€๋ถ„์˜ CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  display: flex;
  justify-content: center;
`;

export const Toggle = () => {
  const [isOn, setIsOn] = useState(false);

  const toggleHandler = () => {
    // TODO : isOn์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
    setIsOn(!isOn);
  };

  return (
    <>
      <ToggleContainer
      // TODO : ํด๋ฆญํ•˜๋ฉด ํ† ๊ธ€์ด ์ผœ์ง„ ์ƒํƒœ(isOn)๋ฅผ boolean ํƒ€์ž…์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      >
        {/* TODO : ์•„๋ž˜์— div ์—˜๋ฆฌ๋จผํŠธ 2๊ฐœ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ๊ฐ์˜ ํด๋ž˜์Šค๋ฅผ 'toggle-container', 'toggle-circle' ๋กœ ์ง€์ •ํ•˜์„ธ์š”. */}
        {/* TIP : Toggle Switch๊ฐ€ ON์ธ ์ƒํƒœ์ผ ๊ฒฝ์šฐ์—๋งŒ toggle--checked ํด๋ž˜์Šค๋ฅผ div ์—˜๋ฆฌ๋จผํŠธ 2๊ฐœ์— ๋ชจ๋‘ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ๋ง์„ ํ™œ์šฉํ•˜์„ธ์š”. */}
        <div
          //! ํ† ๊ธ€ ์ปจํ…Œ์ด๋„ˆ์™€ Circle ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ on/off
          onClick={toggleHandler}
          className={`toggle-container ${isOn ? "toggle--checked" : ""}`}
        />
        <div
          //! ํ† ๊ธ€ ์ปจํ…Œ์ด๋„ˆ์™€ Circle ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ on/off
          onClick={toggleHandler}
          className={`toggle-circle ${isOn ? "toggle--checked" : ""}`}
        />
      </ToggleContainer>
      {/* TODO : Desc ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. */}
      {/* TIP:  Toggle Switch๊ฐ€ ON์ธ ์ƒํƒœ์ผ ๊ฒฝ์šฐ์— Desc ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์˜ ํ…์ŠคํŠธ๋ฅผ 'Toggle Switch ON'์œผ๋กœ, ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ 'Toggle Switch OFF'๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ํ™œ์šฉํ•˜์„ธ์š”. */}
      {isOn === false ? (
        <Desc>Toggle Switch OFF</Desc>
      ) : (
        <Desc>Toggle Switch ON</Desc>
      )}
    </>
  );
};

 

 

 

Tab.js

>

import { useState } from "react";
import styled from "styled-components";

// TODO: Styled-Component ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•ด TabMenu ์™€ Desc ์ปดํฌ๋„ŒํŠธ์˜ CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

const TabMenu = styled.ul`
  background-color: #dcdcdc;
  color: rgba(73, 73, 73, 0.5);
  font-weight: bold;
  display: flex;
  flex-direction: row;
  justify-items: center;
  align-items: center;
  list-style: none;
  margin-bottom: 7rem;

  .submenu {
    ${"" /* ๊ธฐ๋ณธ Tabmenu ์— ๋Œ€ํ•œ CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. */}
    display: flex;
    justify-content: space-around;
    align-items: center;
    width: 230px;
    height: 40px;
  }

  .focused {
    ${"" /* ์„ ํƒ๋œ Tabmenu ์—๋งŒ ์ ์šฉ๋˜๋Š” CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.  */}
    background-color: blueviolet;
    color: #ffffff;
  }

  & div.desc {
    text-align: center;
  }
`;

const Desc = styled.div`
  text-align: center;
`;

export const Tab = () => {
  // TIP: Tab Menu ์ค‘ ํ˜„์žฌ ์–ด๋–ค Tab์ด ์„ ํƒ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ
  // currentTab ์ƒํƒœ์™€ currentTab์„ ๊ฐฑ์‹ ํ•˜๋Š” ํ•จ์ˆ˜๊ฐ€ ์กด์žฌํ•ด์•ผ ํ•˜๊ณ , ์ดˆ๊ธฐ๊ฐ’์€ 0 ์ž…๋‹ˆ๋‹ค.
  const [currentTab, setCurrentTab] = useState(0);

  const menuArr = [
    { name: "Tab1", content: "Tab menu ONE" },
    { name: "Tab2", content: "Tab menu TWO" },
    { name: "Tab3", content: "Tab menu THREE" },
  ];

  const selectMenuHandler = (index) => {
    // TIP: parameter๋กœ ํ˜„์žฌ ์„ ํƒํ•œ ์ธ๋ฑ์Šค ๊ฐ’์„ ์ „๋‹ฌํ•ด์•ผ ํ•˜๋ฉฐ, ์ด๋ฒคํŠธ ๊ฐ์ฒด(event)๋Š” ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค
    // TODO : ํ•ด๋‹น ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋ฉด ํ˜„์žฌ ์„ ํƒ๋œ Tab Menu ๊ฐ€ ๊ฐฑ์‹ ๋˜๋„๋ก ํ•จ์ˆ˜๋ฅผ ์™„์„ฑํ•˜์„ธ์š”.
    setCurrentTab(index);
  };

  return (
    <>
      <div>
        <TabMenu>
          {/*TODO: ์•„๋ž˜ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋‚ด์šฉ ๋Œ€์‹ ์—, map์„ ์ด์šฉํ•œ ๋ฐ˜๋ณต์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.*/}
          {/*TIP: li ์—˜๋ฆฌ๋จผํŠธ์˜ class๋ช…์˜ ๊ฒฝ์šฐ ์„ ํƒ๋œ tab ์€ 'submenu focused' ๊ฐ€ ๋˜๋ฉฐ, 
                  ๋‚˜๋จธ์ง€ 2๊ฐœ์˜ tab์€ 'submenu' ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.*/}
          {menuArr.map((item, index) => {
            return (
              <>
                <li
                  key={index}
                  className={currentTab === index ? "submenu focused" : "submenu"}
                  onClick={() => {selectMenuHandler(index)}}
                >
                  {item.name}
                </li>
              </>
            );
          })}
        </TabMenu>
        <Desc>
          {/*TODO: ์•„๋ž˜ ํ•˜๋“œ์ฝ”๋”ฉ๋œ ๋‚ด์šฉ ๋Œ€์‹ ์—, ํ˜„์žฌ ์„ ํƒ๋œ ๋ฉ”๋‰ด ๋”ฐ๋ฅธ content๋ฅผ ํ‘œ์‹œํ•˜์„ธ์š”*/}
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

 

 

 

 

Tag.js

>

import { useState } from "react";
import styled from "styled-components";

// TODO: Styled-Component ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•ด ์—ฌ๋Ÿฌ๋ถ„๋งŒ์˜ tag ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ๊พธ๋ฉฐ ๋ณด์„ธ์š”!

export const TagsInput = styled.div`
  margin: 8rem auto;
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  min-height: 48px;
  width: 480px;
  padding: 0 8px;
  border: 1px solid rgb(214, 216, 218);
  border-radius: 6px;

  > ul {
    display: flex;
    flex-wrap: wrap;
    padding: 0;
    margin: 8px 0 0 0;

    > .tag {
      width: auto;
      height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      padding: 0 8px;
      font-size: 14px;
      list-style: none;
      border-radius: 6px;
      margin: 0 8px 8px 0;
      background: var(--coz-purple-600);
      > .tag-close-icon {
        display: block;
        width: 16px;
        height: 16px;
        line-height: 16px;
        text-align: center;
        font-size: 14px;
        margin-left: 8px;
        color: var(--coz-purple-600);
        border-radius: 50%;
        background: #fff;
        cursor: pointer;
      }
    }
  }

  > input {
    flex: 1;
    border: none;
    height: 46px;
    font-size: 14px;
    padding: 4px 0 0 0;
    :focus {
      outline: transparent;
    }
  }

  &:focus-within {
    border: 1px solid var(--coz-purple-600);
  }
`;

export const Tag = () => {
  const initialTags = ["CodeStates", "kimcoding"];

  const [tags, setTags] = useState(initialTags);
  const removeTags = (indexToRemove) => {
    // TODO : ํƒœ๊ทธ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ์™„์„ฑํ•˜์„ธ์š”. filter() ์‚ฌ์šฉ
    const removeMethod = tags.filter((el, item) => {
      return item !== indexToRemove;
    });
    setTags(removeMethod);
  };

  const addTags = (event) => {
    // TODO : tags ๋ฐฐ์—ด์— ์ƒˆ๋กœ์šด ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ์™„์„ฑํ•˜์„ธ์š”.
    // ์ด ๋ฉ”์†Œ๋“œ๋Š” ํƒœ๊ทธ ์ถ”๊ฐ€ ์™ธ์—๋„ ์•„๋ž˜ 3 ๊ฐ€์ง€ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    // - ์ด๋ฏธ ์ž…๋ ฅ๋˜์–ด ์žˆ๋Š” ํƒœ๊ทธ์ธ์ง€ ๊ฒ€์‚ฌํ•˜์—ฌ ์ด๋ฏธ ์žˆ๋Š” ํƒœ๊ทธ๋ผ๋ฉด ์ถ”๊ฐ€ํ•˜์ง€ ๋ง๊ธฐ
    // - ์•„๋ฌด๊ฒƒ๋„ ์ž…๋ ฅํ•˜์ง€ ์•Š์€ ์ฑ„ Enter ํ‚ค ์ž…๋ ฅ์‹œ ๋ฉ”์†Œ๋“œ ์‹คํ–‰ํ•˜์ง€ ๋ง๊ธฐ

    const tagValue = event.target.value;

    if (
      tagValue !== "" &&
      event.key === "Enter" &&
      // ํŠน์ •๊ฐ’ ํฌํ•จ ํ™•์ธ ์‹œ, indexOf(), includes() ์‚ฌ์šฉ
      !tags.includes(tagValue)
    ) {
      // ์ž…๋ ฅ๊ฐ’ ์ถ”๊ฐ€
      setTags([...tags, tagValue]);
      // - ํƒœ๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉด input ์ฐฝ ๋น„์šฐ๊ธฐ
      event.target.value = "";
    }
  };

  return (
    <>
      <TagsInput>
        <ul id="tags">
          {tags.map((tag, index) => (
            <li key={index} className="tag">
              <span className="tag-title">{tag}</span>
              <span
                className="tag-close-icon"
                onClick={() => removeTags(index)}
              >
                x
                {/* TODO :  tag-close-icon์ด tag-title ์˜ค๋ฅธ์ชฝ์— x ๋กœ ํ‘œ์‹œ๋˜๋„๋ก ํ•˜๊ณ ,
                            ์‚ญ์ œ ์•„์ด์ฝ˜์„ click ํ–ˆ์„ ๋•Œ removeTags ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. */}
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          /* ํ‚ค๋ณด๋“œ์˜ Enter ํ‚ค์— ์˜ํ•ด addTags ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. */
          onKeyUp={(event) => {
            addTags(event);
          }}
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

 

 

 


 

 

Advanced Challenge (Optional)

 

 

 

ClickToEdit.js

>

import { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';

export const InputBox = styled.div`
  text-align: center;
  display: inline-block;
  width: 150px;
  height: 30px;
  border: 1px #bbb dashed;
  border-radius: 10px;
  margin-left: 1rem;
`;

export const InputEdit = styled.input`
  text-align: center;
  display: inline-block;
  width: 150px;
  height: 30px;
`;

export const InputView = styled.div`
  text-align: center;
  align-items: center;
  margin-top: 3rem;

  div.view {
    margin-top: 3rem;
  }
`;

export const MyInput = ({ value, handleValueChange }) => {
  const inputEl = useRef(null);
  const [isEditMode, setEditMode] = useState(false);
  const [newValue, setNewValue] = useState(value);

	// ์ฒซ ๋ Œ๋”๋ง + [] ๋ฐ”๋€” ๋•Œ ๋ Œ๋”๋ง
  useEffect(() => {
    if (isEditMode) {
      inputEl.current.focus();
    }
  }, [isEditMode]);

  useEffect(() => {
    setNewValue(value);
  }, [value]);

  const handleClick = () => {
    // TODO : isEditMode ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
    setEditMode(true)
  };

  const handleBlur = () => {
    // TODO : Edit๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
    setEditMode(false)
    handleValueChange(newValue);
  };

  const handleInputChange = (e) => {
    // TODO : ์ €์žฅ๋œ value๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
    //? input ์ž…๋ ฅ๊ฐ’ newValue์— ์—…๋ฐ์ดํŠธ
    setNewValue(e.target.value)
  };

  return (
    <InputBox>
      {isEditMode ? (
        <InputEdit
          type='text'
          value={newValue}
          ref={inputEl}
          // TODO : ํฌ์ปค์Šค๋ฅผ ์žƒ์œผ๋ฉด Edit๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ๋˜๋Š” ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
          onBlur={handleBlur}
          // TODO : ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ๊ฐ์ง€๋˜๋ฉด ์ €์žฅ๋œ value๋ฅผ ์—…๋ฐ์ดํŠธ ๋˜๋Š” ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
          onChange={handleInputChange}
        />
      ) : (
        <span 
        // TODO : ํด๋ฆญํ•˜๋ฉด Edit๊ฐ€ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
        onClick={handleClick}
        >{newValue}</span>
      )}
    </InputBox>
  );
}

const cache = {
  name: '๊น€์ฝ”๋”ฉ',
  age: 20
};

export const ClickToEdit = () => {
  const [name, setName] = useState(cache.name);
  const [age, setAge] = useState(cache.age);

  return (
    <>
      <InputView>
        <label>์ด๋ฆ„</label>
        <MyInput value={name} handleValueChange={(newValue) => setName(newValue)} />
      </InputView>
      <InputView>
        <label>๋‚˜์ด</label>
        <MyInput value={age} handleValueChange={(newValue) => setAge(newValue)} />
      </InputView>
      <InputView>
        <div className='view'>์ด๋ฆ„ {name} ๋‚˜์ด {age}</div>
      </InputView>
    </>
  );
};

 

onBlur = {handleBlur}

onBlur : ํฌ์ปค์Šค๊ฐ€ ํ•ด์ง€๋  ๋•Œ ์ด๋ฒคํŠธ ์„ค์ •

<span>์„ ํด๋ฆญํ–ˆ์„ ๋•Œ, ์šฐ๋ฆฌ๋Š” <InputEdit>์—์„œ ๊ธ€์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋‹ค์Œ <span>์„ ํด๋ฆญํ•ด ๊ธ€์„ ์ ์œผ๋ ค๊ณ  ํ–ˆ์„ ๋•Œ, ๋‹ค์‹œ <span>์œผ๋กœ ๋Œ์•„๊ฐ€์•ผํ•œ๋‹ค.

์ด๋•Œ onBlur ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

onBlur ์ด๋ฒคํŠธ๋ฅผ ์“ฐ์ง€ ์•Š์œผ๋ฉด, ๊ทธ๋Œ€๋กœ <InputEdit>์— ๋จธ๋ฌผ๋Ÿฌ ์žˆ๊ฒŒ ๋œ๋‹ค.