[TIL] Day49 #React-Custom-Component

tnqls1211vยท2022๋…„ 7์›” 4์ผ
0

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
49/78
post-thumbnail

2022.07.04(Mon)

[TIL] Day49
[SEB FE] Day48

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

โœ”๏ธย ์˜ค๋Š˜์€ ์ €๋ฒˆ์ฃผ ๊ธˆ์š”์ผ Bare minimum Requirement์— ์ด์–ด Advanced Challenge์„ ๊ตฌํ˜„ํ•ด๋ณด์•˜๋‹ค.
React, Styled Components, Storybook์„ ํ™œ์šฉํ•ด์„œ React-custom-component(Autocomplete Component, ClickToEdit Component)๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ ๐Ÿ™Œ

  1. Autocomplete Component (์ž๋™์™„์„ฑ) : ์ข€ ๊นŒ๋‹ค๋กœ์› ๋˜ ๊ตฌํ˜„.. ๊ธฐ๋ณธ์ ์ธ ์ž๋™์™„์„ฑ ๊ธฐ๋Šฅ + ํ‚ค๋ณด๋“œ ๋ฐฉํ–ฅํ‚ค์™€ ์—”ํ„ฐํ‚ค๋กœ ์˜ต์…˜์„ ์ œ์–ดํ•˜๋Š” ๊ธฐ๋Šฅ๋„ ์‰ฝ์ง€ ์•Š์•˜๋‹ค..๐Ÿซ  ๐Ÿฅฒย enter key๋กœ option์„ ์„ ํƒํ•˜๊ณ  ๊ณ„์† ์ƒ๊ธฐ๋Š” ์ปค์„œ ๊นœ๋นก์ž„์„ ์—†์• ๊ธด ํ–ˆ๋Š”๋ฐ ์—ฌ๋Ÿฌ options์—์„œ ๋ฐฉํ–ฅํ‚ค๋กœ ์„ ํƒํ•  ๋•Œ ๋งˆ๋‹ค input ์•ˆ์˜ ์ปค์„œ๊ฐ€ ์•ž๋’ค๋กœ ์›€์ง์ด๋Š” ํ˜„์ƒ์€ ํ•ด๊ฒฐํ•˜์ง€ ๋ชปํ•จ..
     import { useState, useEffect, useRef } from "react";
     import styled from "styled-components";
     
     const deselectedOptions = [ // ์ž๋™์™„์„ฑ Options List
       "rustic",
       "antique",
       "vinyl",
       "vintage",
     	// ...
     ];
     
     export const InputContainer = styled.div`
       // ... CSS ์ƒ๋žต
     `;
     
     export const DropDownContainer = styled.ul`
       // ... CSS ์ƒ๋žต
     `;
     
     export const Autocomplete = () => {
     
       const [hasText, setHasText] = useState(false);    // input๊ฐ’ ์กด์žฌ ์œ ๋ฌด
       const [inputValue, setInputValue] = useState(""); // input๊ฐ’ ์ƒํƒœ 
       const [options, setOptions] = useState(deselectedOptions); // input๊ฐ’์„ ํฌํ•จํ•˜๋Š” ์ž๋™์™„์„ฑ ์ถ”์ฒœ ํ•ญ๋ชฉ ๋ฆฌ์ŠคํŠธ
       const [keys, setKeys] = useState(0); // ํ‚ค๋ณด๋“œ ๋ฐฉํ–ฅํ‚ค์— ๋Œ€ํ•œ ์ƒํƒœ
       const blurInput = useRef(); // input ๋ฐ•์Šค์—์„œ์˜ ์ปค์„œ ๊นœ๋ฐ•์ž„ ์ƒํƒœ
     
       useEffect(() => {
     		// ๋‚ด์šฉ์„ ์ง€์šฐ๊ณ  ๋‹ค๋ฅธ ๊ฐ’์„ input์— ๋„ฃ์—ˆ์„ ๋•Œ ์ „์— ๋ฐฉํ–ฅํ‚ค๋กœ ๋จธ๋ฌผ๋ €๋˜ ์˜ต์…˜ ์œ„์น˜๊ฐ€ ๊ทธ๋Œ€๋กœ ์žˆ๋Š” ํ˜„์ƒ์„ ์ดˆ๊ธฐํ™”์‹œ์ผœ์คŒ
         setKeys(0); 
         if (inputValue === "") {
           setHasText(false);
         }
       }, [inputValue]);
     
       const handleInputChange = (event) => {
         setInputValue(event.target.value); // ์ž…๋ ฅํ•˜๋Š” ๊ฐ’์œผ๋กœ input๊ฐ’ ๋ณ€๊ฒฝ
         setHasText(true); // ์ž…๋ ฅ์‹œ input๊ฐ’์ด ์กด์žฌํ•˜๋ฏ€๋กœ true๋กœ ๊ฐ’ ๋ณ€๊ฒฝ
         setOptions( // options ์ƒํƒœ์— ๋”ฐ๋ผ drowdown์— ๋ณด์—ฌ์ง€๋Š” ํ•ญ๋ชฉ์ด ๋‹ฌ๋ผ์ง€๋„๋ก filtering
           deselectedOptions.filter(
             (el) => event.target.value === el.slice(0, event.target.value.length)
           )
         );
         // setOptions(deselectedOptions.filter(el => el.includes(event.target.value))); // ์œ„์™€ ๊ฐ™์€ ๊ธฐ๋Šฅ
       };
     
     	// dropdown์— ์ œ์‹œ๋œ ํ•ญ๋ชฉ์„ ๋ˆŒ๋ €์„ ๋•Œ, input๊ฐ’์ด ํ•ด๋‹น ํ•ญ๋ชฉ์˜ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝ๋˜๋Š” ํ•จ์ˆ˜
       const handleDropDownClick = (clickedOption) => {
         setInputValue(clickedOption);
       };
     
       const handleDeleteButtonClick = () => {
         setInputValue(""); // input๊ฐ’์ด ๋นˆ ๋ฌธ์ž์—ด์ด ๋˜๋„๋ก ๊ฐ’ ๋ณ€๊ฒฝ
       };
     
     	// input ๋ฐ•์Šค์— ์ปค์„œ ๊นœ๋ฐ•์ž„ ์—†์• ๋Š” ํ•จ์ˆ˜
       const onBlur = () => {
         blurInput.current.blur();
       };
     
       const handleKeyUp = (event) => { 
         if (hasText) {
           if (event.keyCode === 38) { // ํ‚ค๋ณด๋“œ ๋ฐฉํ–ฅํ‚ค(์ƒ) ์ž…๋ ฅํ–ˆ์„ ๋•Œ
             // event.key === 'ArrowUp'
             if (keys > 0) {
               setKeys(keys - 1);
             }
           } else if (event.keyCode === 40) { // ํ‚ค๋ณด๋“œ ๋ฐฉํ–ฅํ‚ค(ํ•˜) ์ž…๋ ฅํ–ˆ์„ ๋•Œ
             // event.key === 'ArrowDown'
             if (keys < options.length - 1) {
               setKeys(keys + 1);
             }
           }
         }
     
         if (event.key === "Enter") { // ํ‚ค๋ณด๋“œ Enterํ‚ค ์ž…๋ ฅํ–ˆ์„ ๋•Œ
           setInputValue(options[keys]);
           setHasText(false);
           onBlur();
         }
       };
     
       return (
         <div className="autocomplete-wrapper">
           <InputContainer>
             <input
               value={inputValue}
               onChange={handleInputChange}
               onKeyUp={handleKeyUp}
               ref={blurInput}
             ></input>
             <div className="delete-button" onClick={handleDeleteButtonClick}>
               &times;
             </div>
           </InputContainer>
           {hasText ? ( // input๊ฐ’์ด ์กด์žฌํ•˜๋ฉด dropdown ๋ณด์ด๊ฒŒ!
             <DropDown
               options={options}
               handleComboBox={handleDropDownClick}
               keys={keys}
             />
           ) : (
             null
           )}
         </div>
       );
     };
     
     export const DropDown = ({ options, handleComboBox, keys }) => {
       return (
         <DropDownContainer>
           {/* input๊ฐ’์— ๋งž๋Š” ์ž๋™์™„์„ฑ ์„ ํƒ ์˜ต์…˜์„ map์œผ๋กœ ๋ฟŒ๋ ค์คŒ */}
           {options.map((el, idx) => {
             return (
               <li
                 key={idx} // map ์‚ฌ์šฉ์‹œ ๊ผญ key ์ž‘์„ฑํ•ด์ฃผ๊ธฐ!
                 className={idx === keys ? "marked" : ""} // ์„ ํƒ๋œ ์˜ต์…˜ ํ•ญ๋ชฉ ๋ฐฐ๊ฒฝ์ƒ‰ ์„ค์ • ํด๋ž˜์Šค ์ ์šฉ
                 onClick={() => handleComboBox(el)}
               >
                 {el}
               </li>
             );
           })}
         </DropDownContainer>
       );
     };

  1. ClickToEdit Component

    : ์ž๋™์™„์„ฑ์— ๋น„ํ•ด ์‰ฌ์šด ํŽธ์ด์—ˆ๋‹ค~!
    ๐Ÿ˜ˆย input ๋ฐ•์Šค๋ฅผ ํด๋ฆญํ•˜๋ฉด ๊ฐ’์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์„ ์ฐธ๊ณ ํ•ด์„œ CRUD ํ”Œ์ ์—์„œ ๋ฏธ์ฒ˜ ๊ตฌํ˜„ํ•˜์ง€ ๋ชปํ–ˆ๋˜ Update ๊ธฐ๋Šฅ์„ ์ด๋Ÿฐ ์‹์œผ๋กœ ์ ‘๊ทผํ•˜๋ฉด ๋˜๋ ค๋‚˜..?

 import { useEffect, useState, useRef } from "react";
 import styled from "styled-components";
 
 export const InputBox = styled.div` // ๋ชจ๋“  ๋‚ด์šฉ์„ ๋‹ด์„ ์ œ์ผ ํฐ container
   // ... CSS ์ƒ๋žต
 `;
 
 export const InputEdit = styled.input` // input box
   // ... CSS ์ƒ๋žต
 `;
 
 export const InputView = styled.div` // input box์„ ๋‹ด์„ ๊ฐ๊ฐ์˜ container
   // ... CSS ์ƒ๋žต
 `;
 
 export const MyInput = ({ value, handleValueChange }) => {
   const inputEl = useRef(null);
   const [isEditMode, setEditMode] = useState(false); // ์ˆ˜์ •ํ•  ๋ชจ๋“œ on/off
   const [newValue, setNewValue] = useState(value); // ๋ณ€๊ฒฝํ•  ๊ฐ’์„ ๊ด€๋ฆฌํ•  ์ƒํƒœ
 
   useEffect(() => {
     if (isEditMode) { // ์ˆ˜์ •๋ชจ๋“œ์ด๋ฉด
       inputEl.current.focus(); // input box์— ์ปค์„œ ๊นœ๋นก์ž„ focus
     }
   }, [isEditMode]); // ์ˆ˜์ •๋ชจ๋“œ์ผ ๋•Œ๋งŒ ๋ Œ๋”๋ง
 
   useEffect(() => {
     setNewValue(value);
   }, [value]); // value๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ๋ Œ๋”๋ง
 
   const handleClick = () => { // ํด๋ฆญํ–ˆ์„ ๋•Œ ์ด๋ฒคํŠธ
     setEditMode(!isEditMode);
   };
 
   const handleBlur = () => { // ์ˆ˜์ • ๋ชจ๋“œ OFF
     handleValueChange(newValue);
     setEditMode(false);
   };
 
   const handleInputChange = (e) => {
     setNewValue(e.target.value); // value ๊ฐ’ ๋ณ€๊ฒฝ
   };
 
   return (
     <InputBox>
       {isEditMode ? ( // ์ˆ˜์ •๋ชจ๋“œ์ด๋ฉด <InputEdit />
         <InputEdit
           type="text"
           value={newValue}
           ref={inputEl}
           onBlur={handleBlur}
           onChange={handleInputChange}
         />
       ) : (
 				// ์ˆ˜์ •๋ชจ๋“œ๊ฐ€ ์•„๋‹ˆ๋ฉด ์ˆ˜์ •๋œ ๊ฐ’ ๋˜๋Š” ๊ทธ๋ƒฅ ๊ฐ’ ๋ณด์—ฌ์ฃผ๊ธฐ
         <span 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>
     </>
   );
 };

โžฐย Advanced Challenge 13/13 Test Pass โœŒ๏ธ



โ˜‘๏ธย ์ข…ํ•ฉ Quiz

โŒ˜ย CSS in JS

  • CSS ์ž‘์„ฑ์„ ์œ„ํ•ด ์—ฌ๋Ÿฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ โ†’ ๋ฒˆ๋“ค์˜ ํฌ๊ธฐ ์ปค์ง โฌ†๏ธ
  • ๋นŒ๋“œ์‹œ Class๋ช…์ด ์œ ๋‹ˆํฌํ•œ ํ•ด์‹œ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„์˜ ๋ช…๋ช… ๊ทœ์น™ ํ•„์š” โŒ
  • ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด JS์˜ CSS ์ฝ”๋“œ๋ฅผ ์ฝ์–ด์™€ ํŒŒ์‹ฑํ•˜๋Š” ๋‹จ๊ณ„๋ถ€ํ„ฐ ์‹œ์ž‘ โ‡’ CSS-in-CSS์— ๋น„ํ•ด CSS ์ ์šฉ ๋Š๋ฆผ

โŒ˜ Storybook

  • CDD(Component Driven Development)๋ฅผ ํ•˜๊ธฐ์œ„ํ•œ ๋„๊ตฌ
  • ์ปดํฌ๋„ŒํŠธ ์žฌ์‚ฌ์šฉ์„ฑ, ํ…Œ์ŠคํŠธ, ๊ฐœ๋ฐœ ์†๋„๋ฅผ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Œ
  • ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฌธ์„œํ™” ๋ฐ ์‹œ๊ฐํ™”ํ•  ์ˆ˜ ์žˆ์Œ
  • ๋…๋ฆฝ์ ์ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ์‹คํ–‰ โ†’ UI ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง‘์ค‘์ ์œผ๋กœ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ

โŒ˜ useRef

: React์—์„  DOM ์ง์ ‘ ๊ฑด๋“ค ๊ธˆ์ง€ โ†’ document.querySelector โ€ฆ ์‚ฌ์šฉ โŒ
โ†’ But, DOM ๊ฐ์ฒด ์ฃผ์†Œ ํ•„์š”ํ•œ ์ƒํ™ฉ ๋ฐœ์ƒ โ‡’ useRef() ํƒ„์ƒ ๐Ÿฃ

  • DOM ๋…ธ๋“œ, Element, React ์ปดํฌ๋„ŒํŠธ ์ฃผ์†Œ๊ฐ’ ์ฐธ์กฐ ๊ฐ€๋Šฅ
  • useRef๋ฅผ ํ†ตํ•ด DOM์„ ์ง์ ‘ ์กฐ์ž‘์‹œ ๋ฆฌ๋ Œ๋”๋ง์ด ๋˜์ง€ ์•Š์Œ
  • useRef๋ฅผ ํ†ตํ•ด ๊ฐ’ ์ €์žฅ์‹œ ๋ฆฌ๋ Œ๋”๋ง ํ›„, ๊ฐ’ ์ดˆ๊ธฐํ™” โŒย โ‡’ ๋ Œ๋”๋ง ์—ฌ๋ถ€ ์ƒ๊ด€์—†์ด ๊ฐ’์„ ์œ ์ง€ํ•˜๊ณ  ์‹ถ์„๋•Œ ์‚ฌ์šฉ

์•„์ƒท์ถ”๋กœ ์นดํŽ˜์ธ ์ถฉ์ „ํ–ˆ๋Š”๋ฐ๋„ ์˜ค๋Š˜ ํ•˜๋ฃจ ๋„˜๋„˜ ์กธ๋ฆฌ๋‹ค ๐Ÿซ 
๋งจ๋‚  ๋ฏธ๋ฃจ๊ณ  ๋ฏธ๋ค„์„œ ์•„์ง ํ•œ๋ฒˆ๋„ ์•ˆ๋“ค์€ ์•Œ๊ณ ๋ฆฌ์ฆ˜&์ž๋ฃŒ๊ตฌ์กฐ ๊ฐ•์˜ ์˜ค๋Š˜์€ ๊ผญ๊ผญ ๋“ฃ๊ธฐ ๐ŸŽง
๋‚ด์ผ์€ ์ €๋…์— ๊ณต๋ถ€ ๋ชปํ•˜๋‹ˆ๊นŒ ์˜ค๋Š˜ ๋ฐฐ๋กœ ์—ด์‹ฌํžˆ ํ•˜์ž๊ตฌ ๐Ÿค

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