๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Frontend Dev/๐Ÿฅ ์ฝ”๋“œ์Šคํ…Œ์ด์ธ  FE ๋ถ€ํŠธ์บ ํ”„

Section3 Unit3 [React] Custom Component - ๊ณผ์ œ React Custom Component (Bare Minimum)

๋ฐ˜์‘ํ˜•

์ถœ์ฒ˜: https://uxdesign.cc/selection-controls-ui-component-series-3badc0bdb546


Section3 Unit3 [React] Custom Component
- ๊ณผ์ œ React Custom Component (Bare Minimum)

 

โญ๏ธ ๊ณผ์ œ. React Custom Component

๐Ÿ”ฅ ํ•™์Šต๋ชฉํ‘œ

React, Styled-Component, Storybook์„ ํ™œ์šฉํ•ด UI ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ

• Styled Components๋ฅผ ํ™œ์šฉํ•ด ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์˜ ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

• Storybook์„ ์‚ฌ์šฉํ•ด ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๐Ÿท๏ธ Bare Minimum Requirement: Modal, Toggle, Tab, Tag

 

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

→ ๋ชฐ๋ž๊ฑฐ๋‚˜ ํ—ท๊ฐˆ๋ ธ๋˜ ๋ถ€๋ถ„ ์œ„์ฃผ๋กœ โœจ๋ฉ”๋ชจ

 

1. Modal

Modal UI ์ปดํฌ๋„ŒํŠธ๋Š” ๊ธฐ์กด์˜ ๋ธŒ๋ผ์šฐ์ € ํŽ˜์ด์ง€ ์œ„์— ์ƒˆ๋กœ์šด ์œˆ๋„์šฐ ์ฐฝ์ด ์•„๋‹Œ, ๋ ˆ์ด์–ด๋ฅผ ๊นŒ๋Š” ๊ฒƒ์„ ๋งํ•œ๋‹ค.

 

 


• ํŒ์—…๊ณผ ๋ชจ๋‹ฌ

ํŒ์—… Popup

ํŒ์—…์ฐฝ์€ Popํ•˜๊ณ  ํŠ€์–ด๋‚˜์˜ค๋Š”(up) ์›น ํŽ˜์ด์ง€ ํ‘œ์‹œ ๋ฐฉ๋ฒ•์„ ๊ฐ€๋ฆฌํ‚จ๋‹ค. ์ƒˆ ์ฐฝ์„ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด ๊ธฐ์กด ํŽ˜์ด์ง€ ์ „ํ™˜ ๋ฐฉ์‹์ด ์•„๋‹Œ, ์ƒˆ๋กœ์šด ์›น ๋ธŒ๋ผ์šฐ์ € ์ฐฝ์„ ํ•˜๋‚˜ ๋” ์ถ”๊ฐ€์‹œํ‚ค๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ์ฃผ ์šฉ๋„๋Š” ๊ด‘๊ณ , ์•Œ๋ฆผ, ์ƒˆ๋กœ์šด ์ •๋ณด ๊ฐฑ์‹  ๋“ฑ์ด ์žˆ๋‹ค.

ํŒ์—…์€ ํŠธ๋ž˜ํ”ฝ์„ ์ฆ๊ฐ€์‹œํ‚ค๊ธฐ๋„ ํ•˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ฐจ์ง€ํ•˜๋Š” ์ผ๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์š”์ฆ˜์€ ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋””ํดํŠธ ๊ฐ’์œผ๋กœ ํŒ์—…์ฐฝ ์ฐจ๋‹จ ๊ธฐ๋Šฅ์„ ์ง€์›ํ•œ๋‹ค.

→ ์ตœ๊ทผ ํŒ์—…์€ ๋งŽ์ด ์—†์–ด์ง€๋Š” ์ถ”์„ธ์ด๋‹ค.

 

๋ชจ๋‹ฌ Modal

์›น์—์„œ ์ƒˆ ์ฐฝ์„ ๋„์šฐ๋Š” ํŒ์—…์ฐฝ๊ณผ๋Š” ๋‹ฌ๋ฆฌ ๊ฐ™์€ ๋ธŒ๋ผ์šฐ์ € ๋‚ด๋ถ€์—์„œ ์ƒ์œ„ ๋ ˆ์ด์–ด๋ฅผ ๋„์šฐ๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋Š” ์ฐฝ์ด๋‹ค. ๋ชจ๋‹ฌ ์ฐฝ์ด ๋„์›Œ์ง„ ๊ฒฝ์šฐ, ์ด ์ฐฝ์„ ์ข…๋ฃŒํ•˜์ง€ ์•Š๋Š” ์ด์ƒ ์›๋ž˜์˜ ํ™”๋ฉด์„ ์ž‘๋™ํ•  ์ˆ˜ ์—†๋‹ค. ๋”ฐ๋ผ์„œ ์‚ฌ์šฉ์ž์˜ ์ด๋ชฉ์„ ์ง‘์ค‘์‹œํ‚ค๊ธฐ ๋•Œ๋ฌธ์— ์‹ ์ค‘ํžˆ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค.

→ ๋ชจ๋‹ฌ์€ ์šฉ์–ด ์ž์ฒด๋ฅผ ๊ธฐ์—…๋ณ„๋กœ ๋‹ค๋ฅด๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ•˜๋Š”๋ฐ, ๊ตฌ๊ธ€์—์„œ๋Š” ๋‹ค์ด์–ผ๋กœ๊ทธ, ์• ํ”Œ์€ ๋ชจ๋‹ฌ๋ฆฌํ‹ฐ๋ผ๋Š” ์šฉ์–ด๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

 


โœจ event.stopPropagation

{ isOpen && (
  <ModalBackdrop onClick={openModalHandler}>
    <ModalView onClick={(e) => e.stopPropagation()}>
      <p>Modal Opened!</p>
      <button onClick={openModalHandler}>โŒ</button>
    </ModalView>
  </ModalBackdrop>
)}

๐Ÿ’ฌ ModalView ์ปดํฌ๋„ŒํŠธ๋ฅผ ํด๋ฆญํ•ด๋„ openModalHandler ์ด๋ฒคํŠธ๊ฐ€ ์‹คํ–‰๋˜๋Š” ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง ํ˜„์ƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด event.stopPropagation()์„ ์‚ฌ์šฉ

 

 

2. Toggle

Toggle UI ์ปดํฌ๋„ŒํŠธ๋Š” ๋‘ ๊ฐ€์ง€ ์ƒํƒœ๋งŒ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์Šค์œ„์น˜์ด๋‹ค.

 

โœจ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง

// ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ์ฝ”๋“œ
<div className={isOn ? "toggle-container toggle--checked" : "toggle-container"} />
<div className={isOn ? "toggle-circle toggle--checked" : "toggle-circle"} />

// ๋ผ์ด๋ธŒ ์„ธ์…˜์—์„œ ์ž‘์„ฑํ•œ ์ฝ”๋“œ
<div className={`toggle-container${isOn ? " toggle--checked" : ""}`} />
<div className={`toggle-circle${isOn ? " toggle--checked" : ""}`} />

๐Ÿ’ฌ ํ…œํ”Œ๋ฆฟ ๋ฆฌํ„ฐ๋Ÿด์„ ํ™œ์šฉํ•˜๋ฉด ์ข€ ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค!

 

 

3. Tab

Tab UI ์ปดํฌ๋„ŒํŠธ๋Š” ๋™์ผํ•œ ๋ฉ”๋‰ด ๋ผ์ธ์—์„œ ๋ทฐ๋ฅผ ์ „ํ™˜ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

 

โœจ ๊ตฌ์กฐ๋ถ„ํ•ดํ• ๋‹น๊ณผ ๋ฆฌ์ŠคํŠธ ๋ Œ๋”๋ง

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);
};

// ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ์ฝ”๋“œ
{ menuArr.map((el, index) => (
  <li 
    key={index} 
    id={index} 
    className={ currentTab === index ? "submenu focused" : "submenu" }
    onClick={() => selectMenuHandler(index)}
  >
    {el.name}
  </li>
)) }

// ๋ผ์ด๋ธŒ ์„ธ์…˜์—์„œ ์ž‘์„ฑํ•œ ์ฝ”๋“œ

{ menuArr.map(({ name }, index) => (
  <li
    key={name}
    className={`submenu${currentTab === index ? " focused" : ""}`}
		
    // ์•„๋ž˜ ๋‘ ์ฝ”๋“œ๋Š” ์˜ฌ๋ฐ”๋ฅธ ๊ฐ’์ด ๋‚˜์˜ค์ง€ ์•Š๋Š”๋‹ค. 
    // className={`submenu ${currentTab === index ? "focused" : ""}`}
    // className={`submenu ${currentTab === index && "focused"}`}

    onClick={() => selectMenuHandler(index)}
  >
    {name}
  </li>
)) }

๐Ÿ’ฌ menuArr์˜ name์„ ๊ตฌ์กฐ๋ถ„ํ•ดํ• ๋‹นํ•˜์—ฌ ์ „๋‹ฌํ•˜๋Š” ๋ถ€๋ถ„์ด ์ƒˆ๋กœ์› ๋‹ค. ์ƒ๊ฐํ•ด๋ณด์ง€ ๋ชปํ–ˆ๋˜ ๋ถ€๋ถ„์ด๋‹ค.

๐Ÿ’ฌ onClick={() => selectMenuHandler(index)} ์ด ๋ถ€๋ถ„์—์„œ index๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๋ถ€๋ถ„์ด ํ—ท๊ฐˆ๋ ธ์—ˆ๋‹ค. onClick ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ต๋ช…ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๊ณ , ์ด ์ต๋ช… ํ•จ์ˆ˜๋Š” selectMenuHandler๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. ์ด ๋•Œ map ๋ฉ”์„œ๋“œ์˜ ๋‘๋ฒˆ์งธ ์ธ์ˆ˜์ธ index๋ฅผ ํ•จ๊ป˜ ์ „๋‹ฌํ•œ๋‹ค. ์ „๋‹ฌ๋œ index๋Š” selectMenuHandler ํ•จ์ˆ˜์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›๊ฒŒ ๋˜๊ณ , ์ด index๋ฅผ ๋ฐ›์•„ currentTab์„ ๋ณ€๊ฒฝํ•œ๋‹ค.

 

๐Ÿ’ฌ ↓ className ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง

// ๋ฐ”๋ฅธ์ฝ”๋“œ
className={`submenu${currentTab === index ? " focused" : ""}`}

// ์ž˜๋ชป๋œ ์ฝ”๋“œ 1
className={`submenu ${currentTab === index ? "focused" : ""}`}
// ์œ„ ์ฝ”๋“œ ์‹คํ–‰ ๊ฒฐ๊ณผ ์•„๋ž˜์™€ ๊ฐ™์ด class๊ฐ€ ์ง€์ •๋œ๋‹ค.
<li class="submenu focused">Tab1</li>
<li class="submenu ">Tab2</li>

// ์ž˜๋ชป๋œ ์ฝ”๋“œ 2
className={`submenu ${currentTab === index && "focused"}`}
// ์œ„ ์ฝ”๋“œ ์‹คํ–‰ ๊ฒฐ๊ณผ ์•„๋ž˜์™€ ๊ฐ™์ด class๊ฐ€ ์ง€์ •๋œ๋‹ค.
<li class="submenu focused">Tab1</li>
<li class="submenu false">Tab2</li>

 

 

4. Tag

Tag UI ์ปดํฌ๋„ŒํŠธ๋Š” ๋ ˆ์ด๋ธ” ์ง€์ •์„ ํ†ตํ•ด ๊ตฌ์„ฑ์ด๋‚˜ ๋ถ„๋ฅ˜์— ๋„์›€์ด ๋˜๋Š” ํ‚ค์›Œ๋“œ ์ง‘ํ•ฉ์„ ๋งŒ๋“ค ๋•Œ ์ž์ฃผ ์‚ฌ์šฉ๋œ๋‹ค.

 

โœจ ์ด๋ฏธ ์ž…๋ ฅ๋˜์–ด ์žˆ๋Š” ํƒœ๊ทธ์ธ์ง€ ๊ฒ€์‚ฌํ•˜์—ฌ ์ด๋ฏธ ์žˆ๋Š” ํƒœ๊ทธ๋ผ๋ฉด ์ถ”๊ฐ€ํ•˜์ง€ ๋ง๊ธฐ

let addedValue = event.target.value;

// ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ์ฝ”๋“œ
if (addedValue.length < 1) {
  console.log("์•„๋ฌด๊ฒƒ๋„ ์ž…๋ ฅ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
  return;
}

for (let tag of tags) {
  if (tag === addedValue) {
    console.log("๋‹ค๋ฅธ ๊ฐ’์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.");
    return;
  }
}

// ๋ผ์ด๋ธŒ ์„ธ์…˜ ์ฝ”๋“œ
if (addedValue === "") return;
if (tags.includes(addedValue)) return;

๐Ÿ’ฌ for๋ฌธ๊ณผ if๋ฌธ์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด์„œ ๊ฐ™์€ ๊ฐ’์„ ์ฐพ๋Š” ๋Œ€์‹  includes๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ„๋‹จํ•˜๊ฒŒ ๋๋‚  ์ฝ”๋“œ์˜€๋‹ค!

๊ทธ๋ฆฌ๊ณ , ๋‚˜๋Š” ์ฝ˜์†”์„ ์ฐ์–ด์ค€๋‹ค๊ณ  ์—ฌ๋Ÿฌ์ค„ ๊ฑธ์ณ์„œ ์ ๊ธฐ๋Š” ํ–ˆ๋Š”๋ฐ, ๋ผ์ด๋ธŒ์„ธ์…˜ ๋•Œ ๊ฐ•์‚ฌ๋‹˜์€ ํ•œ์ค„๋กœ ๊น”๋”ํžˆ ๋๋ƒˆ๋˜ ์ฝ”๋“œ..! ๊ฒฐ๋ก ์ ์œผ๋กœ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ ์–ด์ฃผ๋ฉด ๋˜์ง€ ์•Š์„๊นŒ?

if (addedValue.length < 1 || tags.includes(addedValue)) {
  console.log("์•„๋ฌด๊ฒƒ๋„ ์ž…๋ ฅ๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜, ์ด๋ฏธ ์žˆ๋Š” ๊ฐ’์ž…๋‹ˆ๋‹ค.")
  return;
}

 

โœจ removeTags

const removeTags = (indexToRemove) => {
  // TODO : ํƒœ๊ทธ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ์™„์„ฑํ•˜์„ธ์š”.
  const newTags = tags.filter((_, idx) => idx !== indexToRemove);
  setTags(newTags)
};

๐Ÿ’ฌ ์กฐ๊ธˆ ํ—ค๋งธ๋˜ ๋ถ€๋ถ„!

๐Ÿ’ฌ _, ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ธ์ˆ˜๋ฅผ ์ด๋ ‡๊ฒŒ ํ‘œํ˜„ํ•ด์„œ ๊ฐ€๋…์„ฑ์„ ๋†’์ด๊ณ , ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ์„ ์•Œ๋ ค์ฃผ๊ธฐ๋„ ํ•œ๋‹ค๊ณ  ํ•œ๋‹ค. ์ƒˆ๋กœ์šด ์‚ฌ์‹ค!

 


๐ŸŒ™  ์˜ค๋Š˜์˜ ํšŒ๊ณ 

 styled component์™€ ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™œ์šฉํ•ด์„œ ์ปค์Šคํ…€ ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“œ๋Š” ์—ฐ์Šต์„ ํ–ˆ๋‹ค. ์ฃผ๋ง์— ์กฐ๊ธˆ ๊ตฌํ˜„ํ•ด๋ด์„œ, ์ƒ๊ฐ๋„ ์ถฉ๋ถ„ํžˆ ํ•˜๋ฉฐ ์กฐ๊ธˆ ๋„๋„ํ•˜๊ฒŒ ์ง„ํ–‰ํ–ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. ๊ตฌํ˜„ ๋„์ค‘์— ์Šคํ† ๋ฆฌ๋ถ์ด ๊ถ๊ธˆํ•ด์„œ ์˜คํ›„ ์„ธ์…˜ ๋•Œ ์งˆ๋ฌธ์„ ํ–ˆ์—ˆ๋Š”๋ฐ ์Šคํ† ๋ฆฌ๋ถ ๊ตณ์ด ๋„ˆ๋ฌด ์‹ ๊ฒฝ์“ฐ์ง€ ๋ง๋ผ๋Š” ์ด์•ผ๊ธฐ๋ฅผ ํ–ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. ์ฃผ๋ง๋™์•ˆ ๊ถ๊ธˆํ•ด์„œ ์ด๊ฒƒ์ €๊ฒƒ ์ฐพ์•„๋ณด๊ณ ๋„ ์ต์ˆ™์น˜ ์•Š์•˜๋˜ ์Šคํ† ๋ฆฌ๋ถ์ด์—ˆ๋Š”๋ฐ, ์˜ค์ „ ์„ธ์…˜๋•Œ๋„, ์˜คํ›„ ์„ธ์…˜๋•Œ๋„ ๊นŠ๊ฒŒ ํŒŒ์ง€ ์•Š์•„๋„ ๋œ๋‹ค๊ณ  ํ•˜๋‹ˆ ์‹œ๊ฐ„ ํˆฌ์ž๋ฅผ ๋‹ค๋ฅธ ๊ณณ์— ํ•˜๊ธฐ๋กœ ๋“œ๋””์–ด ๊ฒฐ์‹ฌํ–ˆ๋‹ค. ์ƒ๊ฐํ•ด๋ณด๋ฉด ์›ํ‹ฐ๋“œ ๊ฐ•์˜๋ฅผ ๋“ค์„ ๋•Œ๋„ ์Šคํ† ๋ฆฌ๋ถ์ด ์ฃผ์ œ์˜€์ง€๋งŒ, ์Œ… ๐Ÿง ํ•„์ˆ˜๊ฑฐ๋‚˜ ๊ฐ•๋ ฅ ์ถ”์ฒœํ•˜๋˜๊ฑด ์•„๋‹ˆ์—ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค. ๋‚˜์ค‘์— ํšŒ์‚ฌ์—์„œ ์“ฐ๋ฉด ๊ทธ๋•Œ ๋‹ค์‹œ ๊ณต๋ถ€ํ•ด๋ด์•ผ์ง€. ์•ฝ๊ฐ„ ํฅ๋ฏธ๊ฐ€ ์ƒ๊ฒผ๊ณ , ์ž˜ ์‚ฌ์šฉํ•ด๋ณด๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ ์ง€๊ธˆ์€ ๋•Œ๊ฐ€ ์•„๋‹Œ๊ฐ€๋ณด๋‹ค ~.~ ๋ฆฌ์•กํŠธ ๊ณต๋ถ€ํ•  ๊ฒƒ๋„ ๋งŽ๊ณ … ํ ..

๋ฐ˜์‘ํ˜•