React Custom Component 중에서 가장 난이도가 높았던 autocomplete이다.
상당히 코드가 길고 관련있는 함수와 state들이 섞여있어 처음부터 감을 잡기가 어려웠다.
하지만, 포기할 수는 없지
될때까지 하는 스타일인 나는 하나하나 코드를 뜯어보며 이해하기로 했다
어떤 리액트 코드를 볼때, 그 코드를 보고 바로 구조를 잡기란 쉽지 않다.
그래서 3가지 기준을 가지고 구조를 파악한다
첫번째, 컴포넌트끼리 tree 구조를 만든다
두번째, state가 어디서 정의되어서 어디로 흐르는가?
세번째, 함수가 어디서 정의되어서 어디로 흐르는가?
이 3가지 기준을 고려하면서 간단한 그림을 그려보았다.
(쉽지는 않았다... 애썻다 내자신)
코드는 너무 기니까, styled component와 react 부분을 나누어서 보겠다
<styled component 부분>
const deselectedOptions = [
'rustic',
'antique',
'vinyl',
'vintage',
'refurbished',
'신품',
'빈티지',
'중고A급',
'중고B급',
'골동품'
];
/* TODO : 아래 CSS를 자유롭게 수정하세요. */
const boxShadow = '0 4px 6px rgb(32 33 36 / 28%)';
const activeBorderRadius = '1rem 1rem 0 0';
const inactiveBorderRadius = '1rem 1rem 1rem 1rem';
export const InputContainer = styled.div`
margin-top: 8rem;
background-color: #ffffff;
display: flex;
flex-direction: row;
padding: 1rem;
border: 1px solid rgb(223, 225, 229);
border-radius: ${(props)=> props.hasText ? activeBorderRadius : inactiveBorderRadius};
// 얘도 autocomplete의 자식컴포넌트 구나! 그래서 props를 통해 props.hasText의 값을 받았고,
// 텍스트가 있으면, activeBorderRadius가 적용
// 텍스트가 없으면, activeBorderRadius가 미적용
z-index: 3;
box-shadow: 0;
&:focus-within {
box-shadow: ${boxShadow};
}
> input {
flex: 1 0 0;
background-color: transparent;
border: none;
margin: 0;
padding: 0;
outline: none;
font-size: 16px;
}
> div.delete-button {
cursor: pointer;
}
`;
export const DropDownContainer = styled.ul`
background-color: #ffffff;
display: block;
margin-left: auto;
margin-right: auto;
list-style-type: none;
margin-block-start: 0;
margin-block-end: 0;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 0px;
margin-top: -1px;
padding: 0.5rem 0;
border: 1px solid rgb(223, 225, 229);
border-radius: 0 0 1rem 1rem;
box-shadow: ${boxShadow};
z-index: 3;
//li 중에서 classname이 selected 인게 있으면 스타일줘
// li {} 안에 '&.' 기호를 작성해서 classname selected의 스타일 설정
> li {
padding: 0 1rem;
&.selected {
background: yellow;
}
}
`;
다음은 리액트 부분이다
(react 부분)
import { useState, useEffect } from 'react';
import styled from 'styled-components';
const deselectedOptions = [
'rustic',
'antique',
'vinyl',
'vintage',
'refurbished',
'신품',
'빈티지',
'중고A급',
'중고B급',
'골동품'
];
/* TODO : 아래 CSS를 자유롭게 수정하세요. */
const boxShadow = '0 4px 6px rgb(32 33 36 / 28%)';
const activeBorderRadius = '1rem 1rem 0 0';
const inactiveBorderRadius = '1rem 1rem 1rem 1rem';
export const InputContainer = styled.div`
margin-top: 8rem;
background-color: #ffffff;
display: flex;
flex-direction: row;
padding: 1rem;
border: 1px solid rgb(223, 225, 229);
border-radius: ${(props)=> props.hasText ? activeBorderRadius : inactiveBorderRadius};
// 얘도 autocomplete의 자식컴포넌트 구나! 그래서 props를 통해 props.hasText의 값을 받았고,
// 텍스트가 있으면, activeBorderRadius가 적용
// 텍스트가 없으면, activeBorderRadius가 미적용
z-index: 3;
box-shadow: 0;
&:focus-within {
box-shadow: ${boxShadow};
}
> input {
flex: 1 0 0;
background-color: transparent;
border: none;
margin: 0;
padding: 0;
outline: none;
font-size: 16px;
}
> div.delete-button {
cursor: pointer;
}
`;
export const DropDownContainer = styled.ul`
background-color: #ffffff;
display: block;
margin-left: auto;
margin-right: auto;
list-style-type: none;
margin-block-start: 0;
margin-block-end: 0;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 0px;
margin-top: -1px;
padding: 0.5rem 0;
border: 1px solid rgb(223, 225, 229);
border-radius: 0 0 1rem 1rem;
box-shadow: ${boxShadow};
z-index: 3;
//리스트 중에서 classname이 selected 인게 있으면 스타일줘
> li {
padding: 0 1rem;
&.selected {
background: yellow;
}
}
`;
export const Autocomplete = () => {
const [hasText, setHasText] = useState(false);
// 입력창 내용유무 state
const [inputValue, setInputValue] = useState('');
// 입력창에 친 내용 state
const [options, setOptions] = useState(deselectedOptions);
// 자동완성 옵션창 state
const [selected, setSelected]= useState(-1)
// 자동완성 옵션창에서 key 사용 인덱스 state
//키보드로 option을 선택할 때 필요한 selected 상태
// 아무것도 선택하지 않았을때(초기값)를 -1으로 두고,
// 하향키를 누르면 인덱스가 +1 씩되고,
// 상향키를 누르면 인덱스가 -1 씩 되는 형태
// 세로줄 리스트 라고 생각하기
// useEffect를 아래와 같이 활용할 수도 있습니다.
useEffect(() => {
if (inputValue === '') {
setHasText(false);
// useEffect가 입력창에 내용이 없을때 hasText를 false로 바꿔주는 걸 전담할게
// 그러니 hasText가 true로 변하는 것만 신경써줘
}
}, [inputValue]);
// TODO : input과 dropdown 상태 관리를 위한 handler가 있어야 합니다.
// input 값 변경시 호출되는 이벤트 핸들러
const handleInputChange = (event) => { // input 값이 변경될때
setInputValue(event.target.value) //내가 적은 대로 입력창에 써짐
setHasText(true) // 드롭다운 창을 보여줌
setOptions(deselectedOptions.filter((item)=>item.includes(event.target.value)))
//입력창에 적은 내용을 포함하는 요소들만 모아서 배열을 만들어줌
};
const handleDropDownClick = (clickedOption) => { //dropdown에서 사용하면 될듯
setInputValue(clickedOption)
setHasText(false) // 드롭다운 창을 안보이게
// HasTexts는 단어그대로 해석하면 안됨. 여기서는 드롭다운 창을 안보이게하는 조건문을 만족시키기 위해서
// setHasText(false)를 설정한 것임
};
const handleDeleteButtonClick = () => {
setInputValue('') //버튼누르면 내용이 지워짐
};
// Advanced Challenge:
// 상하 화살표 키 입력 시 dropdown 항목을 선택하고,
// Enter 키 입력 시 input값을 선택된 dropdown 항목의 값으로 변경하는 handleKeyUp 함수를 만들고,
// 적절한 컴포넌트에 onKeyUp 핸들러를 할당합니다. state가 추가로 필요한지 고민하고, 필요 시 state를 추가하여 제작하세요.
// handleKeyUp 이벤트 핸들러는 키보드에서 키를 눌렀을 때 발생하는 이벤트입니다. 이 이벤트 핸들러는 눌린 키의 정보를 받아와서 처리할 수 있습니다.
//키보드로 option을 선택할 때 필요한 selected 상태를 만듦
// 아무것도 선택하지 않았을때(초기값)를 -1으로 두고,
// 하향키를 누르면 인덱스가 +1 씩되고,
// 상향키를 누르면 인덱스가 -1 씩 되는 형태
// 세로줄 리스트 라고 생각하기
const handleKeyUp = (event) =>{ // 키보드에서 키를 눌렀을때
if(event.key === "ArrowUp" && selected > -1){
// 인덱스를 무한정 -1 씩 할 수 없음, 초기값인 -1보단 크게 제한시켜줘야 함
setSelected(selected -1)
}
if(event.key === "ArrowDown" && selected < options.length -1){
// 인덱스를 무한정 +1 씩할 수 없음, (드롭다운 배열의 길이 -1)보단 작도록 제한시켜줘야 함
// 예를들어, options.length가 3이라면, 인덱스는 +2에서 끝남,
// 그러니 키 다운이 되려면 인덱스는 2보다 작아야함
setSelected(selected +1)
}
else if(event.key === "Enter"){ // enter key
//입력창도 옵션들 중에서 선택된옵션으로 변경 (타자를 치는게 아니라 리스트 중에서 고르는 것이기 때문에 배열을 써야함)
//엔터키를 누르면 옵션들 중에서 하나가 선택되고, 옵션창이 사라짐
setInputValue(options[selected]) //ex. '중고A급'
setHasText(false) // 드롭다운 창을 안보이게
// HasTexts는 단어그대로 해석하면 안됨. 여기서는 드롭다운 창을 안보이게하는 조건문을 만족시키기 위해서
// setHasText(false)를 설정한 것임
}
}
return (
<div className='autocomplete-wrapper'>
<InputContainer hasText ={hasText} options={options}>
{/*왜 InputContainer가 hasText를 가져야 하는건데? 위쪽에서 InputContainer에게 hasText props를 넘겨주었기 때문에*/}
<input value={inputValue} onChange={handleInputChange} onKeyUp={handleKeyUp} />
{/* value 속성은 이 입력 필드의 현재 값 */}
<div onClick={handleDeleteButtonClick} className='delete-button'>×</div>
</InputContainer>
{/*
hasText가 존재할때만 드롭다운(옵션창)을 보여주련~
*/}
{
hasText
? <DropDown selected={selected} options={options} handleDropDownClick={handleDropDownClick}/> : null
}
{/* 요소를 클릭하거나, 엔터키를 쳤을때 <DropDown/> 컴포넌트가 뜨지 않도록
handleDropDownClick 함수와 handleKeyUp 함수가 hasText를 false 값을 갖도록 처리해줘야함*/}
</div>
);
};
export const DropDown = ({ options, handleDropDownClick, selected }) => {
return (
<DropDownContainer>
{/* options를 부모컴포넌트인 Autocomplete에서 받아온다 */}
{
options.map((item, index)=>
<li className={index === selected ? 'selected': ""} onClick = {()=> handleDropDownClick(item)} key ={index}>{item}</li>)
// 클릭하면 클릭한 아이템에 대해서 handleDropDownClick 함수를 실행시켜줌
}
</DropDownContainer>
);
};
자주했던 실수
넘겨받아야 할 파라미터가 있을때는 콜백함수 형태로 작성해준다
{() => { 코드 쓰기 }}와 같은 형태로 이벤트 핸들러를 작성하는 것은 문법적으로 잘못된 것은 아닙니다.
이 방식은 화살표 함수 내에서 여러 줄의 코드를 실행하고 싶을 때 사용됩니다.
하지만 이 방식은 코드가 복잡해질 경우 가독성이 떨어지기 때문에, 보통은 {() => 코드 쓰기}와 같이 간결한 형태로 이벤트 핸들러를 작성하는 것이 좋습니다.
마지막에 걸린 문제와 그 해결
Q. 위의 코드에서 드롭다운 창에서 특정 요소를 클릭하면, 드롭다운 창이 닫혀야 하는데, 열려있어.
어떻게 해결할 수 있을까?
gpt: 드롭다운 창이 닫히지 않는 문제는 handleDropDownClick 함수에서 setOptions 함수를 호출하여 options 값을 변경하면서 발생하는 것 같습니다. setOptions 함수를 호출하는 대신, setInputValue 함수를 호출하여 inputValue 값을 변경하고, hasText 값을 false로 변경하면 됩니다. 이렇게 수정하면 드롭다운 창이 닫히게 됩니다. 수정된 handleDropDownClick 함수의 코드는 다음과 같습니다.
const handleDropDownClick = (clickedOption) => {
setInputValue(clickedOption);
setHasText(false);
};
그래서 setHasText(false)를 함수 handleDropDownClick과 함수 handleKeyUp의 enter 부분에 넣어주었다
'React' 카테고리의 다른 글
코드를 읽는 순차적인 흐름과 오류처리에 관하여 (Cmarket Redux) (6) | 2023.04.25 |
---|---|
Redux란? ('store', 'reducer', 'action' ) (0) | 2023.04.24 |
Cmarket Hooks (프로젝트 구조 파악의 중요성) (0) | 2023.04.21 |
props와 state의 차이점 (0) | 2023.04.21 |
React Custom Component 만들기 (모달, 토글, 탭) (0) | 2023.04.19 |