42일차 React Custom Component2

2021. 9. 16. 23:45
반응형

2021. 09. 15 수요일

1. Today's Key Points!🔑

  • Autocomplete
  • ClickToEdit

2. Sprint과제 복기!🧐

전날에이어 오늘은 Advanced를 복기해볼 것이다.

👉🏻Autocomplete

오토컴플릿 컴포넌트는 검색창에 input값을 입력하면 밑에 input 값과 유사한 추천 검색 옵션을 보여주는 자동 완성 기능이다. 이 부분에서는 상태를 관리해주는 적절한 로직을 잘 짜주는 것이 핵심이다.

import { useState, useEffect } from 'react';
import styled from 'styled-components';

const deselectedOptions = [
  'rustic',
  'antique',
  'vinyl',
  'vintage',
  'refurbished',
  '신품',
  '빈티지',
  '중고A급',
  '중고B급',
  '골동품'
];

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: ${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:hover {
    background-color: lightgray;
    }  

  > li {
    padding: 0 1rem;

    &.selected {
      background-color: lightgray;
    }
  }
`;

export const Autocomplete = () => {
  const [hasText, setHasText] = useState(false); //input값의 유무 상태
  const [inputValue, setInputValue] = useState('');//input값의 상태
  const [options, setOptions] = useState(deselectedOptions);//option의 상태는 input값을 포함하는 autocomplete 추천 항목 리스트를 확인하기 위함
  const [selected, setSelected] = useState(-1);//키보드로 option 선택할때 필요한 selected상태

  useEffect(() => { 
    if (inputValue === '') { //처음 렌더링 됐을 때의 상태와, input값을 모두 지워줬을 때
      setHasText(false); //input값의 유무상태를 false(없음)으로
      setOptions([]);//option은 빈배열로 만들어서 아래에 리스트가 나타나지 않도록 구현
    }

    if(inputValue !== ''){ //input값을 입력하면
    setOptions(deselectedOptions.filter((el) => { //입력된 값을 포함하는 option만 걸러준 상태로 변경한다.
      return el.includes(inputValue)
      })
    )
  }
  }, [inputValue]);

  const handleInputChange = (event) => { //input값 변경 시 발생되는 이벤트 핸들러. 
    setInputValue(event.target.value); //inputValue를 입력된 값으로 바꿔준다.
    setHasText(true); //input값 유무상태도 당연히 true(있음)으로 바꿔준다.
  };

  const handleDropDownClick = (clickedOption) => { //DropDown 컴포넌트의 li엘리먼트에서 onClick으로 이벤트 핸들러 함수에 option을 전달해주고 있다.
    setInputValue(clickedOption) //전달받은 option으로 inputValue를 변경해준다.
  };

  const handleDeleteButtonClick = (event) => { //x 버튼 누르면
    setInputValue(""); //input입력창을 비워준다.
  };

  const handleKeyUp = (event) => { //option을 키보드로 선택할 수 있게해주는 핸들러 함수
    if(hasText){ //input에 값이 있을때
      if(event.key === 'ArrowDown' && options.length - 1 > selected){ 
        setSelected(selected + 1);
      }
      //options.length에 -1을 해주는 이유는 selected의 최대값을 맞춰주기 위해서이다.
      //예를들어 밑에 option이 2개가 나왔다고 가정했을 때, selected값이 최대 1까지 변할 수 있게 해줘야한다. 
      //'ArrowDown'키를 누르면 selected는 0이 되고, 한번 더 누르면 1이 되고, 그 다음은 더이상 옵션이 없기 때문에 키가 안먹히게 해주는 것이다.

      if(event.key === 'ArrowUp' && selected >= 0){ //처음 조건을 이해했다면 여기는 자연스럽게 이해될 것이다.
        setSelected(selected - 1);
      }
      if(event.key === 'Enter' && selected >= 0){ //Enter키로 option 선택
        handleDropDownClick(options[selected])
        setSelected(-1); //Enter키를 눌러서 선택이 되면 다시 selected는 -1이 되야한다.
      }
    }
  }

  return (
    <div className='autocomplete-wrapper'>
      <InputContainer >
        <input type="text" 
          value={inputValue} 
          defaultValue={inputValue} 
          onChange={handleInputChange}
          onKeyUp={handleInputChange}>
        </input>
        <div className='delete-button' 
             onClick={handleDeleteButtonClick}
             >&times;</div>
      </InputContainer>
      //input에 값이없으면 DropDown이 보이지 않게 해준 것이다.
      {hasText && <DropDown options={options} 
                            handleComboBox={handleDropDownClick}
                            selected={selected}/>}
      //위의 코드는 다음과 같다.
      {/*hasText ? <DropDown options={options}
                             handleComboBok={handleDropDownClick}
                             selected={selected}/>
                 : null */}
    </div>
  );
};

export const DropDown = ({ options, handleComboBox, selected }) => {
  return (
    <DropDownContainer>
        {options.map((option, idx) => {
          return <li 
                   key={idx}
                   onClick={() => handleComboBox(option)}
                   className={selected === idx ? 'selected' : ''}
                   >{option}</li>
        })}
    </DropDownContainer>
  );
};

options를 변경해주는 부분을 useEffect에서 관리하니까 그냥 inputValue가 변할때 마다 알아서 options가 바뀌다보니 편하게 관리해줄 수 있었다. useEffect에서 관리를 해주지 않았다면 inputValue를 변하게 해주는 함수마다 options도 변하게 해주는 함수를 구현해주었어야 할 것이다.

Autocomplete 구현 결과

👉🏻ClickToEdit

ClickToEdit 컴포넌트는 input 창을 클릭하면 수정이 가능하고, input 창이 아닌 곳을 클릭하면 수정한 내용이 반영되는 기능을 가진 컴포넌트이다. 이 부분에서는 useRef를 사용해서 input창을 클릭했을 때만 값을 바꿀 수 있게 해주는 것이 핵심이다.

import { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';

export const InputViewContainer = styled.div`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100%;
  position: absolute;
  /* left: 42%; */
`
export const InputBox = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  /* text-align: center; */
  /* display: inline-block; */
  width: 150px;
  height: 30px;
  border: 1px #bbb dashed;
  border-radius: 10px;
  margin-left: 1rem;
`;

export const InputEdit = styled.input`
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  display: inline-block;
  width: 150px;
  height: 30px;
`;

export const InputView = styled.div`
  display: flex;
  justify-content: center;
  text-align: center;
  align-items: center;
  margin: 1rem;

  div.view {
    margin-top: 0.5rem;
  }
`;

//MyInput 컴포넌트는 ClickToEdit 컴포넌트의 자식 컴포넌트이다.
//그래서 value를 전달 받는데 여기(value)에는 { name, age } 로 name상태값과 age상태값을 가지고 있다.
export const MyInput = ({ value, handleValueChange }) => {
  const inputEl = useRef(null);
  const [isEditMode, setEditMode] = useState(false); //edit모드 상태
  const [newValue, setNewValue] = useState(value); //출력값 상태

  useEffect(() => {
    if (isEditMode) { //edit모드가 활성화 되면 input창에 포커스를 줘서 수정이 가능하도록 해준다.
      inputEl.current.focus();
    }
  }, [isEditMode]);

  useEffect(() => {
    setNewValue(value);
  }, [value]);

  const handleClick = () => { //span태그를 클릭하면 edit모드가 활성화 되고 위의 useEffect에 의해 input창에 포커싱이 된다.
    setEditMode(true);
  };

  const handleBlur = () => { //input창이 아닌 다른 곳을 클릭하면 edit모드를 비활성화로 만든다.
    setEditMode(false);
    handleValueChange(newValue); //그리고 input창에 입력되어있는 값으로 newValue를 바꿔준다.
  };

  const handleInputChange = (e) => {
    setNewValue(e.target.value); //input에 입력한 값을 newValue에 담아둔다.
    //여기서 입력을 해준다고 바로바로 밑의 출력값이 변하지 않는다.
    //왜냐하면 handleBlur에 의해서 handleValueChange 함수가 실행되어야 값이 바뀌기 때문이다.
  };

  return (
    <InputBox>
    //edit모드가 활성화 되면 input태그가 되고, 비활성이 되면 span태그가 된다.
      {isEditMode ? (
        <InputEdit
          type='text'
          value={newValue}
          ref={inputEl}
          onBlur={handleBlur}
          onChange={handleInputChange}
        />
      ) : (
        <span onClick={handleClick}>{newValue}</span>
      )}
    </InputBox>
  );
}

const cache = {
  name: '홍길동',
  age: 18
};

export const ClickToEdit = () => {
  const [name, setName] = useState(cache.name);
  const [age, setAge] = useState(cache.age);

  return (
    <>
    <InputViewContainer>
      <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>
      </InputViewContainer>
    </>
  );
};

Autocomplete를 구현하는 것 보다는 상대적으로 수월했다. 그리고 이 컴포넌트의 CSS는 app.css를 건드리면서 맞춰준 CSS라서 app.css를 적절하게 바꿔주지 않은 상태에서 위의 코드를 그대로 사용하면 디자인이 이상해질 수 있음을 주의하자.

반응형
LIST

BELATED ARTICLES

more