Jieunny์˜ ๋ธ”๋กœ๊ทธ

S3) Unit 3. [์‹ค์Šต] Styled Component ๋กœ UI ๊ตฌํ˜„ํ•˜๊ธฐ ๋ณธ๋ฌธ

CodeStates/Training

S3) Unit 3. [์‹ค์Šต] Styled Component ๋กœ UI ๊ตฌํ˜„ํ•˜๊ธฐ

Jieunny 2023. 2. 21. 18:00

๐Ÿ“ฃ  ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” UI๋ฅผ Styled Component๋กœ ๊ตฌํ˜„ํ•ด๋ณด์ž

1๏ธโƒฃ  Modal

โœ”๏ธ ์กฐ๊ฑด

      โœ“ Modal ์ฐฝ์„ ๋„์›Œ ์ค„ Open Modal ๋ฒ„ํŠผ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (24 ms)
      โœ“ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด Modal ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— Modal๋ฐฐ๊ฒฝ, Modal์ฐฝ div ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (162 ms)
      โœ“ Modal์ฐฝ์ด ๋ Œ๋”๋ง ๋œ ์ƒํƒœ์—์„œ ๋ฒ„ํŠผ์„ ๋‹ค์‹œ ํด๋ฆญํ•˜๋ฉด Modal๋ฐฐ๊ฒฝ, Modal์ฐฝ div ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค. (84 ms)
      โœ“ Modal ์ฐฝ ๋ฐ–์„ ํด๋ฆญํ•˜๋ฉด, Modal๋ฐฐ๊ฒฝ, Modal์ฐฝ div ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค. (46 ms)

 

โœ”๏ธ ๊ตฌํ˜„ ์ฝ”๋“œ

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

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

export const ModalBackdrop = styled.div`
  // TODO : Modal์ด ๋–ด์„ ๋•Œ์˜ ๋ฐฐ๊ฒฝ์„ ๊น”์•„์ฃผ๋Š” CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  position: absolute;
  justify-content: center;
  align-items: center;
  background-color: rgb(0,0,0,0.3);
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  width: 100%;
  height: 100%;
`;

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๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: white;
  flex-direction: column;
  width: 50%;
  height: 50%;
  border-radius: 20px;
  > .text {
    font-size: 2rem;
    margin-bottom: 10px;
  }
`;

export const ExitBtn = styled(ModalBtn)`
  display: flex;
  justify-content: center;
  align-items: center;
  width: 30%;
  height: 30%;
  font-size: 0.8rem;
`

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

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

//   const handleClickOutside = ({ target }) => {
//     if (isOpen && !modalOutside.current.contains(target)) setIsOpen(false);
//   };

// useEffect(() => {
//   window.addEventListener("click", handleClickOutside);
//   return () => {
//     window.removeEventListener("click", handleClickOutside);
//   };
// }, []);

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

โžฐ ๋ชจ๋‹ฌ ์ฐฝ ๋ฐ”๊นฅ ๋ถ€๋ถ„์„ ๋ˆ„๋ฅด๋ฉด ๋ชจ๋‹ฌ ์ฐฝ์ด ๊บผ์ ธ์•ผ ํ•˜๋Š” ๋ถ€๋ถ„์ด ๊ตฌํ˜„ํ•˜๊ธฐ ๊นŒ๋‹ค๋กœ์› ๋‹ค.

โžฐ ์ง€๊ธˆ๋„ ํ…Œ์ŠคํŠธ๋Š” ํ†ต๊ณผํ–ˆ์ง€๋งŒ ๊น”๋”ํ•˜์ง€ ๋ชปํ•˜๋‹ค. Close ๋ฒ„ํŠผ, ๋ชจ๋‹ฌ ์ฐฝ, ๋ชจ๋‹ฌ ๋ฐ”๊นฅ ๋ถ€๋ถ„์„ ๋ˆŒ๋ €์„ ๋•Œ ์ฐฝ์ด ๊บผ์ง„๋‹ค.

โžฐ ๋”ฑ ๋ชจ๋‹ฌ ๋ฐ”๊นฅ ๋ถ€๋ถ„๋งŒ ๋ˆŒ๋ €์„ ๋•Œ ๊บผ์ง€๊ฒŒ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์„ํ…๋ฐใ… 

=> ์ด๋Ÿฐ ํ˜„์ƒ์„ ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง์ด๋ผ๊ณ  ํ•˜๋ฉฐ, stopPropagation์„ ์‚ฌ์šฉํ•ด์„œ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค!!

(์ž์‹์„ ํด๋ฆญํ–ˆ๋Š”๋ฐ ๋’ค์— ์žˆ๋Š” ๋ถ€๋ชจ๊ฐ€ ํด๋ฆญ๋ฌ๋‹ค๊ณ  ์ธ์‹ํ•ด์„œ ๋ถ€๋ชจ์— ๊ฑธ๋ฆฐ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด๋‹ค)

 

โœ”๏ธ ์‹œ์—ฐ ํ™”๋ฉด


2๏ธโƒฃ  Toggle

โœ”๏ธ ์กฐ๊ฑด

      โœ“ Toggle container๋ฅผ ํด๋ฆญํ•˜๋ฉด 'toggle--checked' class๊ฐ€ ์ถ”๊ฐ€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (29 ms)
      โœ“ Toggle switch๋ฅผ ํด๋ฆญํ•˜๋ฉด 'toggle--checked' class๊ฐ€ ์ถ”๊ฐ€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (14 ms)

 

โœ”๏ธ ๊ตฌํ˜„ ์ฝ”๋“œ

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;
    transition: 0.5s;
  } >.toggle--checked {
      background-color: rgb(55, 0, 195);
      transition: 0.5s;
  }
    // TODO : .toggle--checked ํด๋ž˜์Šค๊ฐ€ ํ™œ์„ฑํ™” ๋˜์—ˆ์„ ๊ฒฝ์šฐ์˜ CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
  

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

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

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

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

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

 

โœ”๏ธ ์‹œ์—ฐ ํ™”๋ฉด


3๏ธโƒฃ  Tab

โœ”๏ธ ์กฐ๊ฑด

    Tab Menu๋Š” map์„ ์ด์šฉํ•œ ๋ฐ˜๋ณต์„ ํ†ตํ•ด ๋ณด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      โœ“ ul ์—˜๋ฆฌ๋จผํŠธ ์•„๋ž˜์—๋Š” li ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ 3๊ฐœ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (20 ms)
    Tab Menu ์กฐ์ž‘์„ ์œ„ํ•œ currentTab ์ƒํƒœ๊ฐ€ ์กด์žฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      โœ“ currentTab ์ดˆ๊ธฐ๊ฐ’์€ 0๋ฒˆ์งธ ์ธ๋ฑ์Šค์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (7 ms)
    Tab Menu๋ฅผ ํด๋ฆญํ•˜๋ฉด currentTab ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
      โœ“ Tab ๋ฉ”๋‰ด๋ฅผ ํด๋ฆญํ•˜๋ฉด selectMenuHandler ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๊ณ , ํ•ด๋‹น Tab ๋ฉ”๋‰ด์˜ index ๊ฐ’์ด ์ธ์ž๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค (7 ms)
      โœ“ ํด๋ฆญํ•œ Tab ๋ฉ”๋‰ด๋งŒ className ์ด "submenu focused"๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. (17 ms)

 

โœ”๏ธ ๊ตฌํ˜„ ์ฝ”๋“œ

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๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. */}
    width: calc(100% / 3);
    display: flex;
    justify-content: center;
    align-items: center;
    height: 50px;

  }

  .focused {
    ${'' /* ์„ ํƒ๋œ Tabmenu ์—๋งŒ ์ ์šฉ๋˜๋Š” CSS๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.  */}
    background-color: rgb(55, 0, 195);
    color: white;
  }

  & 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);
    //console.log(index);
  };

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

 

โœ”๏ธ ์‹œ์—ฐ ํ™”๋ฉด


4๏ธโƒฃ  Tag

โœ”๏ธ ์กฐ๊ฑด

    Enter ํ‚ค ํ…Œ์ŠคํŠธ
      โœ“ ์ƒˆ๋กœ์šด ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ธฐ๋Šฅ์€ Enter ํ‚ค์— ์˜ํ•ด ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (20 ms)
      โœ“ Enter ํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด tag ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” addTags ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (10 ms)
      โœ“ Enterํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ์‹ค์ œ ํƒœ๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (96 ms)
      โœ“ ์•„๋ฌด๊ฒƒ๋„ ์ž…๋ ฅํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, Enter๋ฅผ ๋ˆŒ๋Ÿฌ๋„ ํƒœ๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. (29 ms)
      โœ“ ์ค‘๋ณต๋œ ๊ฐ’์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ, Enter๋ฅผ ๋ˆŒ๋Ÿฌ๋„ ํƒœ๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. (39 ms)
      โœ“ ์ƒˆ๋กœ์šด ํƒœ๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉด ์ž…๋ ฅ์ฐฝ์€ ์ดˆ๊ธฐํ™”๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (9 ms)
    tags์˜ ํ™”๋ฉด ์ถœ๋ ฅ๊ณผ ์ œ๊ฑฐ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ
      โœ“ tags ๋ฐฐ์—ด์˜ ๋ชจ๋“  ํƒœ๊ทธ๊ฐ€ ํ™”๋ฉด์— ๋ณด์—ฌ์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (19 ms)
      โœ“ tag ๋ฅผ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋Š” ์•„์ด์ฝ˜(x)์ด ์žˆ์–ด์•ผ ํ•˜๋ฉฐ, ํ•ด๋‹น ์•„์ด์ฝ˜(x)์„ ํด๋ฆญํ•˜๋ฉด removeTags ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (4 ms)
      โœ“ ์‚ญ์ œ ์•„์ด์ฝ˜์„ ๋ˆ„๋ฅด๋ฉด ํ™”๋ฉด์—์„œ Tag๊ฐ€ ์‚ญ์ œ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (17 ms)

 

โœ”๏ธ ๊ตฌํ˜„ ์ฝ”๋“œ

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 : ํƒœ๊ทธ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ์™„์„ฑํ•˜์„ธ์š”.
    const removeArr = tags.filter((tag, index) => {
      return index !== indexToRemove;
    })
    setTags(removeArr);
  };

  const addTags = (event) => {
    // TODO : tags ๋ฐฐ์—ด์— ์ƒˆ๋กœ์šด ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ์™„์„ฑํ•˜์„ธ์š”.
    // ์ด ๋ฉ”์†Œ๋“œ๋Š” ํƒœ๊ทธ ์ถ”๊ฐ€ ์™ธ์—๋„ ์•„๋ž˜ 3 ๊ฐ€์ง€ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    // - ์ด๋ฏธ ์ž…๋ ฅ๋˜์–ด ์žˆ๋Š” ํƒœ๊ทธ์ธ์ง€ ๊ฒ€์‚ฌํ•˜์—ฌ ์ด๋ฏธ ์žˆ๋Š” ํƒœ๊ทธ๋ผ๋ฉด ์ถ”๊ฐ€ํ•˜์ง€ ๋ง๊ธฐ
    // - ์•„๋ฌด๊ฒƒ๋„ ์ž…๋ ฅํ•˜์ง€ ์•Š์€ ์ฑ„ Enter ํ‚ค ์ž…๋ ฅ์‹œ ๋ฉ”์†Œ๋“œ ์‹คํ–‰ํ•˜์ง€ ๋ง๊ธฐ
    // - ํƒœ๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉด input ์ฐฝ ๋น„์šฐ๊ธฐ
    //console.log(event);
    if(event.key==="Enter" && tags.includes(event.target.value)===false && event.target.value.length !== 0){
      setTags([...tags,event.target.value]);
      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)}>
                {/* TODO :  tag-close-icon์ด tag-title ์˜ค๋ฅธ์ชฝ์— x ๋กœ ํ‘œ์‹œ๋˜๋„๋ก ํ•˜๊ณ ,
                            ์‚ญ์ œ ์•„์ด์ฝ˜์„ click ํ–ˆ์„ ๋•Œ removeTags ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. */}
                            x
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          onKeyUp={(e) => {
            {
              /* ํ‚ค๋ณด๋“œ์˜ Enter ํ‚ค์— ์˜ํ•ด addTags ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. */
              addTags(e)
            }
          }}
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

 

โœ”๏ธ ์‹œ์—ฐ ํ™”๋ฉด


5๏ธโƒฃ  Autocomplete

โœ”๏ธ ์กฐ๊ฑด

   input ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ
      โœ“ input ์—˜๋ฆฌ๋จผํŠธ์— onChange ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ๋ถˆ๋ ค์™€์•ผ ํ•ฉ๋‹ˆ๋‹ค. (9 ms)
      โœ“ input ๊ฐ’์„ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋Š” ๋ฒ„ํŠผ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (5 ms)
      โœ“ ์‚ญ์ œ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ input value๊ฐ€ ์‚ญ์ œ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (47 ms)
    drop down ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ
      โœ“ input ๊ฐ’์ด ํฌํ•จ ๋œ ์ž๋™ ์™„์„ฑ ์ถ”์ฒœ drop down ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋ณด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (8 ms)
      โœ“ drop down ํ•ญ๋ชฉ์„ ๋งˆ์šฐ์Šค๋กœ ํด๋ฆญ ์‹œ, input ๊ฐ’ ๋ณ€๊ฒฝ์— ๋”ฐ๋ผ drop down ๋ชฉ๋ก์ด ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (52 ms)
      โœ“ drop down ํ•ญ๋ชฉ์„ ๋งˆ์šฐ์Šค๋กœ ํด๋ฆญ ์‹œ, input ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (45 ms)
      โœ“ drop down ํ•ญ๋ชฉ์„ ๋งˆ์šฐ์Šค๋กœ ํด๋ฆญ ์‹œ, input ๊ฐ’์ด ์ด๋ฏธ ์žˆ์–ด๋„ input ๊ฐ’์ด drop down ํ•ญ๋ชฉ์˜ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (33 ms)

 

โœ”๏ธ ๊ตฌํ˜„ ์ฝ”๋“œ

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

const deselectedOptions = [
  'rustic',
  'antique',
  'vinyl',
  'vintage',
  'refurbished',
  '์‹ ํ’ˆ',
  '๋นˆํ‹ฐ์ง€',
  '์ค‘๊ณ A๊ธ‰',
  '์ค‘๊ณ B๊ธ‰',
  '๊ณจ๋™ํ’ˆ'
];

/* TODO : ์•„๋ž˜ CSS๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์ˆ˜์ •ํ•˜์„ธ์š”. */
const boxShadow = '0 4px 6px rgb(32 33 36 / 28%)';
const activeBorderRadius = '1rem 1rem 0 0';
const inactiveBorderRadius = '1rem 1rem 1rem 1rem';

export const InputContainer = styled.div`
  margin-top: 8rem;
  background-color: #ffffff;
  display: flex;
  flex-direction: row;
  padding: 1rem;
  border: 1px solid rgb(223, 225, 229);
  border-radius: ${inactiveBorderRadius};
  z-index: 3;
  box-shadow: 0;

  &:focus-within {
    box-shadow: ${boxShadow};
  }

  > input {
    flex: 1 0 0;
    background-color: transparent;
    border: none;
    margin: 0;
    padding: 0;
    outline: none;
    font-size: 16px;
  }

  > div.delete-button {
    cursor: pointer;
  }
`;

export const DropDownContainer = styled.ul`
  background-color: #ffffff;
  display: block;
  margin-left: auto;
  margin-right: auto;
  list-style-type: none;
  margin-block-start: 0;
  margin-block-end: 0;
  margin-inline-start: 0px;
  margin-inline-end: 0px;
  padding-inline-start: 0px;
  margin-top: -1px;
  padding: 0.5rem 0;
  border: 1px solid rgb(223, 225, 229);
  border-radius: 0 0 1rem 1rem;
  box-shadow: ${boxShadow};
  z-index: 3;

  > li {
    padding: 0 1rem;
  }
`;

export const Autocomplete = () => {
  /**
   * Autocomplete ์ปดํฌ๋„ŒํŠธ๋Š” ์•„๋ž˜ 3๊ฐ€์ง€ state๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ํ•„์š”์— ๋”ฐ๋ผ์„œ state๋ฅผ ๋” ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
   * - hasText state๋Š” input๊ฐ’์˜ ์œ ๋ฌด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
   * - inputValue state๋Š” input๊ฐ’์˜ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
   * - options state๋Š” input๊ฐ’์„ ํฌํ•จํ•˜๋Š” autocomplete ์ถ”์ฒœ ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
   */
  const [hasText, setHasText] = useState(false);
  let [inputValue, setInputValue] = useState('');
  const [options, setOptions] = useState(deselectedOptions);

  // useEffect๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ํ™œ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
  useEffect(() => {
    if (inputValue === '') {
      setHasText(false);
    }

  }, [inputValue]);

  // TODO : input๊ณผ dropdown ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ handler๊ฐ€ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  const handleInputChange = (event) => {
    /**
     * handleInputChange ํ•จ์ˆ˜๋Š”
     * - input๊ฐ’ ๋ณ€๊ฒฝ ์‹œ ๋ฐœ์ƒ๋˜๋Š” change ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์ž…๋‹ˆ๋‹ค.
     * - input๊ฐ’๊ณผ ์ƒํƒœ๋ฅผ ์—ฐ๊ฒฐ์‹œํ‚ฌ ์ˆ˜ ์žˆ๊ฒŒ controlled component๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ณ 
     * - autocomplete ์ถ”์ฒœ ํ•ญ๋ชฉ์ด dropdown์œผ๋กœ ์‹œ์‹œ๊ฐ๊ฐ ๋ณ€ํ™”๋˜์–ด ๋ณด์—ฌ์งˆ ์ˆ˜ ์žˆ๋„๋ก ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
     *
     * handleInputChange ํ•จ์ˆ˜๋ฅผ ์™„์„ฑํ•˜์—ฌ ์•„๋ž˜ 3๊ฐ€์ง€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
     *
     * onChange ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ
     * 1. input๊ฐ’ ์ƒํƒœ์ธ inputValue๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
     * 2. input๊ฐ’ ์œ ๋ฌด ์ƒํƒœ์ธ hasText๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
     * 3. autocomplete ์ถ”์ฒœ ํ•ญ๋ชฉ์ธ options์˜ ์ƒํƒœ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
     * Tip : options์˜ ์ƒํƒœ์— ๋”ฐ๋ผ dropdown์œผ๋กœ ๋ณด์—ฌ์ง€๋Š” ํ•ญ๋ชฉ์ด ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค.
     */
    setInputValue(event.target.value);
    setHasText(true);
  };

  const handleDropDownClick = (clickedOption) => {
    /**
     * handleDropDownClick ํ•จ์ˆ˜๋Š”
     * - autocomplete ์ถ”์ฒœ ํ•ญ๋ชฉ์„ ํด๋ฆญํ•  ๋•Œ ๋ฐœ์ƒ๋˜๋Š” click ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์ž…๋‹ˆ๋‹ค.
     * - dropdown์— ์ œ์‹œ๋œ ํ•ญ๋ชฉ์„ ๋ˆŒ๋ €์„ ๋•Œ, input๊ฐ’์ด ํ•ด๋‹น ํ•ญ๋ชฉ์˜ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝ๋˜๋Š” ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
     *
     * handleInputChange ํ•จ์ˆ˜๋ฅผ ์™„์„ฑํ•˜์—ฌ ์•„๋ž˜ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
     *
     * onClick ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ
     * 1. input๊ฐ’ ์ƒํƒœ์ธ inputValue๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
     * 2. autocomplete ์ถ”์ฒœ ํ•ญ๋ชฉ์ธ options์˜ ์ƒํƒœ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
     */
    setInputValue(deselectedOptions[clickedOption]);
  };

  const handleDeleteButtonClick = () => {
    /**
     * handleDeleteButtonClick ํ•จ์ˆ˜๋Š”
     * - input์˜ ์˜ค๋ฅธ์ชฝ์— ์žˆ๋Š” X๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ฐœ์ƒ๋˜๋Š” click ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์ž…๋‹ˆ๋‹ค.
     * - ํ•จ์ˆ˜ ์ž‘์„ฑ์„ ์™„๋ฃŒํ•˜์—ฌ input๊ฐ’์„ ํ•œ ๋ฒˆ์— ์‚ญ์ œํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
     *
     * handleDeleteButtonClick ํ•จ์ˆ˜๋ฅผ ์™„์„ฑํ•˜์—ฌ ์•„๋ž˜ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.
     *
     * onClick ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ
     * 1. input๊ฐ’ ์ƒํƒœ์ธ inputValue๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด์ด ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
     */
    setInputValue('');
  };

  // Advanced Challenge: ์ƒํ•˜ ํ™”์‚ดํ‘œ ํ‚ค ์ž…๋ ฅ ์‹œ dropdown ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜๊ณ , Enter ํ‚ค ์ž…๋ ฅ ์‹œ input๊ฐ’์„ ์„ ํƒ๋œ dropdown ํ•ญ๋ชฉ์˜ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” handleKeyUp ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค๊ณ ,
  // ์ ์ ˆํ•œ ์ปดํฌ๋„ŒํŠธ์— onKeyUp ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค. state๊ฐ€ ์ถ”๊ฐ€๋กœ ํ•„์š”ํ•œ์ง€ ๊ณ ๋ฏผํ•˜๊ณ , ํ•„์š” ์‹œ state๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ œ์ž‘ํ•˜์„ธ์š”.

  return (
    <div className='autocomplete-wrapper'>
      <InputContainer>
        {/* TODO : input ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  input๊ฐ’(value)์„ state์™€ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. handleInputChange ํ•จ์ˆ˜์™€ input๊ฐ’ ๋ณ€๊ฒฝ ์‹œ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ๊ฒŒ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. */}
        <input onChange={handleInputChange} value={inputValue} type="text"></input>
        {/* TODO : ์•„๋ž˜ div.delete-button ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด input ๊ฐ’์ด ์‚ญ์ œ๋˜์–ด dropdown์ด ์—†์–ด์ง€๋Š” handler ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. */}
        <div className='delete-button' onClick={handleDeleteButtonClick}>&times;</div>
      </InputContainer>
      {/* TODO : input ๊ฐ’์ด ์—†์œผ๋ฉด dropdown์ด ๋ณด์ด์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ์ด์šฉํ•ด์„œ ๊ตฌํ˜„ํ•˜์„ธ์š”. */}
      {inputValue ? <DropDown options={options} handleComboBox={handleDropDownClick} input={inputValue} /> : null}
    </div>
  );
};

export const DropDown = ({ options, handleComboBox, input }) => {
  return (
    <DropDownContainer>
      {/* TODO : input ๊ฐ’์— ๋งž๋Š” autocomplete ์„ ํƒ ์˜ต์…˜์ด ๋ณด์—ฌ์ง€๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. */}
      {options && options.map((option, index) => {
        if(option.includes(input)){
          return <li key={index} onClick={() => handleComboBox(index)}>{option}</li>
        }
      })}
    </DropDownContainer>
  );
};

 

โœ”๏ธ ์‹œ์—ฐ ํ™”๋ฉด


6๏ธโƒฃ  ClickToEdit

โœ”๏ธ ์กฐ๊ฑด

    input ์ฐฝ ํด๋ฆญ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ
      โœ“ ์ž…๋ ฅ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” onClick ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ span ์—˜๋ฆฌ๋จผํŠธ์— ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (4 ms)
      โœ“ ํฌ์ปค์Šค๊ฐ€ ์ œ์™ธ๋˜๋Š” ์ด๋ฒคํŠธ onBlur์˜ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ input ์—˜๋ฆฌ๋จผํŠธ์— ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (5 ms)
      โœ“ ํ…์ŠคํŠธ ์˜์—ญ์„ ํด๋ฆญํ•˜๋ฉด ์ž…๋ ฅ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (4 ms)
      โœ“ ์ž…๋ ฅ ๊ฐ€๋Šฅ ์ƒํƒœ์ผ ๋•Œ ๋ณ€ํ™”๊ฐ€ ๊ฐ์ง€๋˜๋ฉด ์ƒˆ๋กœ์šด ๊ฐ’์„ ์„ค์ •ํ•˜๋Š” ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (5 ms)
      โœ“ ์ž…๋ ฅ ๊ฐ€๋Šฅ ์ƒํƒœ์ผ ๋•Œ input์ด ์•„๋‹Œ ๋‹ค๋ฅธ ๊ณณ์„ ํด๋ฆญํ•˜๋ฉด ์ž…๋ ฅ ๋ถˆ๊ฐ€ ์ƒํƒœ๊ฐ€ ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (9 ms)
      โœ“ ์ž…๋ ฅ ๊ฐ€๋Šฅ ์ƒํƒœ์ผ ๋•Œ input์ด ์•„๋‹Œ ๋‹ค๋ฅธ ๊ณณ์„ ํด๋ฆญํ•˜๋ฉด input์˜ ๊ฐ’์ด span์— ๋‹ด๊ฒจ์•ผ ํ•ฉ๋‹ˆ๋‹ค. (10 ms)

 

โœ”๏ธ ๊ตฌํ˜„ ์ฝ”๋“œ

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(!isEditMode);
  };

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

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

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

 

โœ”๏ธ ์‹œ์—ฐ ํ™”๋ฉด


๐Ÿ“š ์ฐธ๊ณ ์ž๋ฃŒ https://infiduk.github.io/2022/09/08/react-onclick.html

 

INFIDUK Blog

์„ฑ๊ณตํ•œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์ž

infiduk.github.io