[TIL] Day48 #React-Custom-Component

Beanxxยท2022๋…„ 7์›” 1์ผ
1

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
48/120
post-thumbnail

2022.07.01(Fri)

[TIL] Day48
[SEB FE] Day47

โ˜‘๏ธย [Pair] React Custom Component

์˜ค๋Š˜์€ ์˜ค๋žœ๋งŒ์— React ์ฝ”๋“œ ๋„์ ๋„์ โœ๏ธ
๋ช‡ ์ผ Express, UI/UX ํ–ˆ๋‹ค๊ณ  ๋ฒŒ์จ ๊ฐ€๋ฌผ๊ฐ€๋ฌผํ•˜๋‹ค ๐Ÿซ 

โœ”๏ธย ์˜ค๋Š˜์˜ Bare minimum Requirement๋Š” React, Styled Components, Storybook์„ ํ™œ์šฉํ•ด์„œ React-custom-component(Modal, Toggle, Tab, Tag)๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ ๐Ÿ™Œ


  1. Modal
    : ๋ชจ๋‹ฌ์—์„œ ์ข€ ์• ๋จน์—ˆ๋˜ ๊ฒƒ์€ ๋ฒ„ํŠผ์ด๋ž‘ ๋ชจ๋‹ฌ์ฐฝ์˜ ์œ„์น˜ ์กฐ์ •..
    โœ๏ธย ์‚ผํ•ญ์—ฐ์‚ฐ์ž๋ฅผ ์ด์šฉํ•ด์„œ ๋ชจ๋‹ฌ์ฐฝ์ด ์—ด๋ ธ์„ ๋•Œ์™€ ๋‹ซํ˜”์„ ๋•Œ ๊ตฌ๋ถ„ํ•ด์„œ ๋ณด์—ฌ์ฃผ๊ธฐ!

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

 // ๋ชจ๋‹ฌ์ฐฝ์„ ๋„์›Œ์ฃผ๋Š” ๊ณต๊ฐ„
 export const ModalContainer = styled.div`
   position: relative; // ๋ถ€๋ชจ ์š”์†Œ์ธ ModalContainer๊ฐ€ ์ž์‹ ์š”์†Œ์ธ ModalBackdrop์˜ ๊ธฐ์ค€์ ์ด ๋จ
   height: 100%;
 `;

 // ๋ชจ๋‹ฌ์ฐฝ์ด ์—ด๋ ธ์„ ๋•Œ์˜ ๋ฐฐ๊ฒฝ
 export const ModalBackdrop = styled.div`
   display: flex;
   justify-content: center;
   align-items: center;
   position: absolute; // ๋ถ€๋ชจ ์š”์†Œ์ธ ModalContainer๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์œ„์น˜ ๊ฒฐ์ •
   top: 0; // ModalContainer์„ ๊ธฐ์ค€์œผ๋กœ top์„ ๋”ฑ ๋ถ™์ด๋Š” ๊ฐœ๋…
   left: 0; // ์œ„์™€ ๊ฐ™์ด ์™ผ์ชฝ, ์˜ค๋ฅธ์ชฝ, ์•„๋ž˜ ๋ชจ๋‘ ModalContainer์— ๋”ฑ ๋ถ™๋„๋ก!
   right: 0;
   bottom: 0;
   background-color: rgba(0, 0, 0, 0.5); // ๋งˆ์ง€๋ง‰ ์ž๋ฆฌ๋Š” ๋ฐฐ๊ฒฝ์ƒ‰ ํˆฌ๋ช…๋„ ์„ค์ •
 `;

 // ํด๋ฆญํ•ด์„œ ๋ชจ๋‹ฌ์ฐฝ์„ ์—ฌ๋Š” ๋ฒ„ํŠผ
 export const ModalBtn = styled.button`
   // ... CSS ์ƒ๋žต 
 	// ์•„๋ž˜๋Š” ๋ฒ„ํŠผ์„ ๊ฐ€์šด๋ฐ ์œ„์น˜ํ•˜๋„๋ก ํ•˜๋Š” ์ฝ”๋“œ
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
 `;

 // ๋ชจ๋‹ฌ์ฐฝ
 export const ModalView = styled.div.attrs((props) => ({
   // attrs() ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด์„œ div element์— ์†์„ฑ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
   role: "dialog", // dialog -> modal ๊ฐœ๋…?
 }))`
   // ... CSS ์ƒ๋žต
 	// ๊ฒ€์ƒ‰ํ•˜๋‹ค๊ฐ€ ๋ง˜์— ๋“  ํ…Œ๋‘๋ฆฌ ๊ทธ๋ฆผ์ž css ์“ฑ์‹น
   box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px,
     rgba(0, 0, 0, 0.3) 0px 30px 60px -30px,
     rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset;

 	// ๋ชจ๋‹ฌ์ฐฝ ์•ˆ์— ์žˆ๋Š” ๋‹ซ๊ธฐ x ์•„์ด์ฝ˜
   > .fa-times-circle {
     margin-bottom: 1rem;
   }
 `;

 export const Modal = () => {
   const [isOpen, setIsOpen] = useState(false); // ๋ชจ๋‹ฌ์ฐฝ open, close ์ƒํƒœ

   const openModalHandler = () => {
     setIsOpen(!isOpen); // ๋ฒ„ํŠผ ํด๋ฆญ์‹œ ๋ชจ๋‹ฌ์ฐฝ์˜ ์ƒํƒœ ๋ฐ”๊ฟ”์ฃผ๊ธฐ
   };

   return (
     <>
       <ModalContainer>
         <ModalBtn onClick={openModalHandler}>
           {isOpen ? "Opened!" : "Open Modal"}
         </ModalBtn>
         {isOpen ? (
           <ModalBackdrop onClick={openModalHandler}>
             <ModalView>
               <i className="fas fa-times-circle" onClick={openModalHandler} />
               Opened Modal!
             </ModalView>
           </ModalBackdrop>
         ) : (
           ""
         )}
       </ModalContainer>
     </>
   );
 };

  1. Toggle
    : โœ๏ธย ํ† ๊ธ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๋‹ค๊ฐ€ ์ƒˆ๋กœ ์•Œ๊ฒŒ ๋œ ๋‚ด์šฉ์€ className์— ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ๋ง ์ ์šฉํ•˜๋Š” ๋ฒ•
 import { useState } from "react";
 import styled from "styled-components";
 
 const ToggleContainer = styled.div`
   position: relative;
   margin-top: 8rem;
   left: 47%;
   cursor: pointer;
   width: 50px;
 
   > .toggle-container {
     width: 45px;
     height: 24px;
     border-radius: 30px;
     background-color: #dad9ff;
     transition: all 1s ease-out; // ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋กœ ์Šค๋ฌด์Šคํ•˜๊ฒŒ ๋ฐฐ๊ฒฝ์ƒ‰ ๋ณ€๊ฒฝ
 
     &.toggle--checked {
       background-color: #4641d9; 
     }
   }
 
   > .toggle-circle {
     position: absolute;
     // ... css ์ƒ๋žต
     transition: all 0.5s ease-out; // ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋กœ ์Šค๋ฌด์Šคํ•˜๊ฒŒ ํ† ๊ธ€ ๋ฒ„ํŠผ์ด ์›€์ง์ž„
 
     &.toggle--checked {
       left: 24px;
     }
   }
 `;
 
 const Desc = styled.div`
   text-align: center;
   margin-top: 1rem;
 `;
 
 export const Toggle = () => {
   const [isOn, setisOn] = useState(false);
 
   const toggleHandler = () => {
     setisOn(!isOn);
   };
 
   return (
     <>
       <ToggleContainer
         onClick={toggleHandler}
 				{/* ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ๋ง ์ ์šฉ */}
         <div className={`toggle-container ${isOn ? "toggle--checked" : ""}`} />
         <div className={`toggle-circle ${isOn ? "toggle--checked" : ""}`} />
       </ToggleContainer>
       <Desc>{isOn ? "Toggle Switch ON" : "Toggle Switch OFF"}</Desc>
     </>
   );
 };

  1. Tab
     import { useState } from "react";
      import styled from "styled-components";
     
     // Tab์„ ๋ณด์—ฌ์ฃผ๋Š” ์ž๋ฆฌ
     const TabMenu = styled.ul`
       // ... css ์ƒ๋žต
     
     	// Tab1, Tab2, Tab3 ๊ฐ๊ฐ์˜ Tab
       .submenu {
         display: flex;
         flex: 1 1 0; // Tab๋ผ๋ฆฌ ๋‹ค๋‹ฅ๋‹ค๋‹ฅ ๋ถ™์–ด์žˆ๋Š” ๋ฌธ์ œ ํ•ด๊ฒฐ
         height: 100%;
         justify-content: center;
         align-items: center;
       }
     
     	// ์„ ํƒํ•œ Tabmenu CSS
       .focused {
         background-color: #4374D9;
         color: white;
         flex: 1 1 0;
       }
     
       & div.desc {
         text-align: center;
       }
     `;
     
     const Desc = styled.div`
       text-align: center;
     `;
     
     export const Tab = () => {
       const [isTab, setIsTab] = 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) => {
         setIsTab(index);
       };
     
       return (
         <>
           <div>
             <TabMenu>
               {menuArr.map((item, idx) => (
                 <li
                   className={`submenu${isTab === idx ? " focused" : ""}`}
                   key={idx} // map ์‚ฌ์šฉ์‹œ์—” ๋ฌด์กฐ๊ฑด key ์„ค์ •!
                   onClick={() => selectMenuHandler(idx)}
                 >
                   {item.name}
                 </li>
               ))}
             </TabMenu>
             <Desc>
               <p>{menuArr[isTab].content}</p>
             </Desc>
           </div>
         </>
       );
     };

  1. Tag
    : ๋‚ด ๊ธฐ์ค€ Tag ๊ตฌํ˜„์ด ์ œ์ผ ๋ญ”๊ฐ€ ๊นŒ๋‹ค๋กœ์› ๋‹ค.. ์—”ํ„ฐ์น˜๋ฉด ๊ฐ’์ด ๋“ค์–ด๊ฐ€๊ณ , x ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์‚ญ์ œ๋˜๋„๋ก ๊ตฌํ˜„ํ•˜๊ธฐ!

 import { useState } from "react";
 import styled from "styled-components";
 
 export const TagsInput = styled.div`
   // ... CSS ์ƒ๋žต
 
   > ul {
     // ... CSS ์ƒ๋žต
 
     > .tag {
       // ... CSS ์ƒ๋žต
 
 			// ํƒœ๊ทธ ๋‚ด์— ์žˆ๋Š” X ํƒœ๊ทธ ์ทจ์†Œ ์•„์ด์ฝ˜ css
       > .tag-close-icon {
         // ... CSS ์ƒ๋žต
         &:hover { // x ์•„์ด์ฝ˜ ์œ„์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ ธ์„ ๋•Œ css
           background-color: #8c8c8c;
           color: white;
         }
       }
     }
   }
 
 	// ํƒœ๊ทธ ์ถ”๊ฐ€ input CSS
   > input {
     // ... CSS ์ƒ๋žต
     :focus {
       outline: transparent;
     }
   }
   &:focus-within {
     border: 1px solid #4000c7;
   }
 `;
 
 export const Tag = () => {
   const initialTags = ["Coding", "kimcoding"];
 
   const [text, setText] = useState("");
   const [tags, setTags] = useState(initialTags);
 
 	// Tag ์‚ญ์ œ
   const removeTags = (indexToRemove) => {
 		// 
     const removes = tags.filter((item, index) => index !== indexToRemove);
     setTags(removes);
   };
 
   const addTags = (event) => {
 		// ์ด๋ฏธ ํƒœ๊ทธ๋กœ ์ถ”๊ฐ€๋˜์žˆ๊ฑฐ๋‚˜ ์•„๋ฌด๊ฒƒ๋„ ์ž…๋ ฅ๋˜์–ด ์žˆ์ง€ ์•Š์•˜์„ ๋• ํƒœ๊ทธ ์ถ”๊ฐ€ XXX
     if (!tags.includes(event) && event.length !== 0) {
       setTags([...tags, event]); // ๊ธฐ์กด ํƒœ๊ทธ์— ์ƒˆ๋กœ์šด ํƒœ๊ทธ ์ถ”๊ฐ€
       setText(""); // ํƒœ๊ทธ ์ถ”๊ฐ€ ํ›„, input ๋น„์šฐ๊ธฐ
     }
   };
 
   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</span>
               </li>
             ))}
         </ul>
         <input
           className="tag-input"
           type="text"
           value={text}
           onChange={(e) => setText(e.target.value)} // ์ž…๋ ฅํ•˜๋Š” ํƒœ๊ทธ ์ƒํƒœ ๋ณ€๊ฒฝ ํ•จ์ˆ˜
           onKeyUp={(e) => {
             if (e.key === "Enter") { // Enter ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ
               addTags(e.target.value); // addTags() ์‹คํ–‰
             }
           }}
           placeholder="Press enter to add tags"
         />
       </TagsInput>
     </>
   );
 };

โžฐย Bare minimum Requirement 19/19 Test Pass โœŒ๏ธ

โœ…ย ๋‹ด์ฃผ ์›”์šœ์—” ์ž๋™์™„์„ฑ ๊ธฐ๋Šฅ๊ณผ input ํด๋ฆญ์‹œ ๋‚ด์šฉ ์ˆ˜์ • ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ ๐Ÿซ 


๋ฒŒ์จ 7์›”์ด๋ผ๋‹ˆ.. 2022๋…„์ด ๋ฒŒ์จ ์ ˆ๋ฐ˜์ด ํ›Œ์ฉ ์ง€๋‚˜๊ฐ”๋‹ค.๐Ÿฅฒย ๋ฐฑ์ˆ˜ ์ƒํ™œ 4๊ฐœ์›”ing ใ€ฐ๏ธ
์š”์ฆ˜ ๋‚ ์”จ๊ฐ€ ๋”์›Œ์„œ ๊ทธ๋Ÿฐ์ง€ ์›๋ž˜ ์•ˆ ์ข‹์•˜๋˜ ์ง‘์ค‘๋ ฅ์ด ๋”๋”๋” ์•ˆ ์ข‹์•„์ง„ ๋Š๋‚Œ์ด๋‹ค,, ๋” ์—ด์‹ฌํžˆ ๋บ˜์ƒค๋บ˜์ƒค ๐Ÿ’ช

[๐Ÿซถ Todo List]
โ˜‘๏ธย ํ”„๋กœ๊ทธ๋ž˜๋จธ์Šค Lv.1_์ •์ˆ˜ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ๋ฐฐ์น˜ํ•˜๊ธฐ
โ˜‘๏ธย Coplit ๊ฐ์ฒด ๋ฌธ์ œ ๋ณต์Šต
โ˜‘๏ธย Udemy ์•Œ๊ณ ๋ฆฌ์ฆ˜&์ž๋ฃŒ๊ตฌ์กฐ ๊ฐ•์˜ (๋น…์˜คํ‘œ๊ธฐ๋ฒ•) ๋“ฃ๊ธฐ
โ˜‘๏ธ Udemy React ๊ฐ•์˜ (Section2) ๋“ฃ๊ธฐ
โ˜‘๏ธ ์ธํ”„๋Ÿฐ ํ•œ์ž… ๋ฆฌ์•กํŠธ - JS์‘์šฉ ๊ฐ•์˜ ๋“ฃ๊ธฐ

profile
FE developer

0๊ฐœ์˜ ๋Œ“๊ธ€