2022.07.04(Mon)
[TIL] Day49
[SEB FE] Day48
โ๏ธย ์ค๋์ ์ ๋ฒ์ฃผ ๊ธ์์ผ Bare minimum Requirement์ ์ด์ด Advanced Challenge์ ๊ตฌํํด๋ณด์๋ค.
React, Styled Components, Storybook์ ํ์ฉํด์React-custom-component
(Autocomplete Component
,ClickToEdit Component
)๋ฅผ ๊ตฌํํด๋ณด๊ธฐ ๐
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}>
×
</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>
);
};
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 โ๏ธ
โย CSS in JS
CSS-in-CSS
์ ๋นํด CSS ์ ์ฉ ๋๋ฆผโ Storybook
โ useRef
: React์์ DOM ์ง์ ๊ฑด๋ค ๊ธ์ง โ document.querySelector โฆ ์ฌ์ฉ โ
โ But, DOM ๊ฐ์ฒด ์ฃผ์ ํ์ํ ์ํฉ ๋ฐ์ โ useRef()
ํ์ ๐ฃ
useRef
๋ฅผ ํตํด DOM์ ์ง์ ์กฐ์์ ๋ฆฌ๋ ๋๋ง์ด ๋์ง ์์useRef
๋ฅผ ํตํด ๊ฐ ์ ์ฅ์ ๋ฆฌ๋ ๋๋ง ํ, ๊ฐ ์ด๊ธฐํ โย โ ๋ ๋๋ง ์ฌ๋ถ ์๊ด์์ด ๊ฐ์ ์ ์งํ๊ณ ์ถ์๋ ์ฌ์ฉ์์ท์ถ๋ก ์นดํ์ธ ์ถฉ์ ํ๋๋ฐ๋ ์ค๋ ํ๋ฃจ ๋๋ ์กธ๋ฆฌ๋ค ๐ซ
๋งจ๋ ๋ฏธ๋ฃจ๊ณ ๋ฏธ๋ค์ ์์ง ํ๋ฒ๋ ์๋ค์ ์๊ณ ๋ฆฌ์ฆ&์๋ฃ๊ตฌ์กฐ ๊ฐ์ ์ค๋์ ๊ผญ๊ผญ ๋ฃ๊ธฐ ๐ง
๋ด์ผ์ ์ ๋ ์ ๊ณต๋ถ ๋ชปํ๋๊น ์ค๋ ๋ฐฐ๋ก ์ด์ฌํ ํ์๊ตฌ ๐ค