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์ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ๋ฅผ ํ์ฉํด์ ์ปค์คํ ์ปดํฌ๋ํธ ๋ง๋๋ ์ฐ์ต์ ํ๋ค. ์ฃผ๋ง์ ์กฐ๊ธ ๊ตฌํํด๋ด์, ์๊ฐ๋ ์ถฉ๋ถํ ํ๋ฉฐ ์กฐ๊ธ ๋๋ํ๊ฒ ์งํํ๋ ๊ฒ ๊ฐ๋ค. ๊ตฌํ ๋์ค์ ์คํ ๋ฆฌ๋ถ์ด ๊ถ๊ธํด์ ์คํ ์ธ์ ๋ ์ง๋ฌธ์ ํ์๋๋ฐ ์คํ ๋ฆฌ๋ถ ๊ตณ์ด ๋๋ฌด ์ ๊ฒฝ์ฐ์ง ๋ง๋ผ๋ ์ด์ผ๊ธฐ๋ฅผ ํ๋ ๊ฒ ๊ฐ๋ค. ์ฃผ๋ง๋์ ๊ถ๊ธํด์ ์ด๊ฒ์ ๊ฒ ์ฐพ์๋ณด๊ณ ๋ ์ต์์น ์์๋ ์คํ ๋ฆฌ๋ถ์ด์๋๋ฐ, ์ค์ ์ธ์ ๋๋, ์คํ ์ธ์ ๋๋ ๊น๊ฒ ํ์ง ์์๋ ๋๋ค๊ณ ํ๋ ์๊ฐ ํฌ์๋ฅผ ๋ค๋ฅธ ๊ณณ์ ํ๊ธฐ๋ก ๋๋์ด ๊ฒฐ์ฌํ๋ค. ์๊ฐํด๋ณด๋ฉด ์ํฐ๋ ๊ฐ์๋ฅผ ๋ค์ ๋๋ ์คํ ๋ฆฌ๋ถ์ด ์ฃผ์ ์์ง๋ง, ์… ๐ง ํ์๊ฑฐ๋ ๊ฐ๋ ฅ ์ถ์ฒํ๋๊ฑด ์๋์๋ ๊ฒ ๊ฐ๋ค. ๋์ค์ ํ์ฌ์์ ์ฐ๋ฉด ๊ทธ๋ ๋ค์ ๊ณต๋ถํด๋ด์ผ์ง. ์ฝ๊ฐ ํฅ๋ฏธ๊ฐ ์๊ฒผ๊ณ , ์ ์ฌ์ฉํด๋ณด๊ณ ์ถ์๋๋ฐ ์ง๊ธ์ ๋๊ฐ ์๋๊ฐ๋ณด๋ค ~.~ ๋ฆฌ์กํธ ๊ณต๋ถํ ๊ฒ๋ ๋ง๊ณ … ํ ..