새소식

프론트엔드 공부/React

React-custom-component

  • -

리엑트 컴토넌트 만들기

css는 정리되지 않아서 나중에 다시 정리해야 할 것 같다.

  • Modal
  • Toggle
  • Tab
  • Tag
  • Autocomplete
  • ClickToEdit

Modal

Open Modal 버튼을 클릭하면 Opened!로 바뀌면서 모달창이 화면에 나타나야 한다. Modal UI 컴포넌트는 기존의 브라우저 페이지 위에 새로운 윈도우 창이 아닌, 레이어를 까는 것을 말한다. 모달창을 구현하면서 CSS의 position과 stopPropagation에 대해 익힐 수 있었다. 

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

export const ModalContainer = styled.div`
  // TODO : Modal을 구현하는데 전체적으로 필요한 CSS를 구현합니다.
  width:100%;
  height:250px;
  display: flex;
  justify-content : center;
  align-items : center;
  position: relative;
`;

export const ModalBackdrop = styled.div`
  position : fixed;
  left: 0;
  top: 0;
  width:100vw;
  height: 100vh;
  background-color :rgba(0,0,0,0.8);
  display : flex;
  justify-content : center;
  align-items : center;
`;

export const ModalBtn = styled.button`
  text-decoration: none;
  border: none;
  padding: 10px 20px;
  color: grey;
  cursor: pointer;
  font-size: 16px;
  transition: 0.1s;
  border-radius: 19px;
  background: #e0e0e0;
  box-shadow:  6px 6px 12px #bebebe,
             -6px -6px 12px #ffffff;
  //&:hover{box-shadow:-2px -2px 0 cyan, 2px 2px 0 magenta;}
`;

export const ModalView = styled.div.attrs((props) => ({
  // attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
  role: 'dialog',
}))`
  // TODO : Modal창 CSS를 구현합니다.
  width: 400px;
  height: 200px;
  border-radius: 10px;
  background-color: #fff;
  display: flex;
  justify-content : center;
  align-items : center;
  position: relative;
  > .modal--content{
    margin: 20px;
  }
  > .modal--content > h3{
    margin-bottom: 20px;
  }
`;

export const BtnClose = styled(ModalBtn)`
  background-color: black;
  color: white;
  margin: 10px;
  padding: 2px 8px;
  border-radius: 4px;
  position: absolute;
  top:10px;
  right: 10px;
`;

export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  const openModalHandler = () => {
    // TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
    setIsOpen(!isOpen);
  };

  return (
    <>
      <ModalContainer>
        <ModalBtn
        // TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
        onClick={openModalHandler}
        >
          {/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
          {isOpen ? '이미 눌렀잖아요':'눌러주세요' }
        </ModalBtn>
        {/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 구현해야 합니다. */}
        {isOpen === true? <ModalBackdrop onClick={openModalHandler}>
          <ModalView onClick={(e)=> e.stopPropagation()}>
          <BtnClose onClick={openModalHandler}>x</BtnClose>
          <div className='modal--content'>
            <h3>아무말 대잔치</h3>
            <p>
              모 달 모 달 달 달 해 별 달고나 나무 무럭무럭자라라 잭과콩나무 콩나물국밥 바오밥나무 
            </p>
          </div>
          </ModalView>
        </ModalBackdrop> : null}
      </ModalContainer>
    </>
  );
};

부모 컴포넌트에 이벤트 핸들러가 걸려있을 때 자식컴포넌트에도 같은 핸들러가 작동이되는데, 이때 자식 컴포넌트에서는 작동을 안하게 해주려면 그 해당 이벤트 핸들러에 stopPropagation()를 활용해주면 된다. 위의 코드에서 onClick={(event) => {event.stopPropagation()} 이렇게 사용을 했고, 이것 때문에 모달창이 떴을 때 배경을 클릭하면 모달창이 꺼지지만, 모달창 내부를 클릭하면 아무일도 일어나지 않는다.


Toggle

두 가지 상태만을 가지고 있는 스위치이다. 이 부분에서는 transition에 대해 익힐 수 있었다

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

export const Wrap = styled.div`
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  border-radius: 10px;
  &.toggle--wrapbg{
    background: var(--darkbg);
    color: #ffffff;
  }
  &.toggle--nowrapbg{
    background: var(--lightbg);
    color:#251D29;
  }
`;

export const ToggleContainer = styled.div`
  position: relative;
  cursor: pointer;
  
  > .toggle-container {
    width: 16em;
    height: 8em;
    border-radius: 4em;
    
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
  } 
  > .toggle--checked {
    background: var(--bgColor--night);
  } .toggle--nochecked{
    background: #FFBF71;
  }

  > .toggle-circle {
    position: absolute;
    top: 0;
    left: 1px;
    display: block;
    width: 50%;
    height: 100%;
    border-radius: 50%;
    transition: all 400ms ease-in-out;
    text-align: center;
    font-size: 12px;
    color:var(--gray-600);
    transition: all 0.3s ease;
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    &.toggle--checked {

    background: var(--bgColor--night);
    transform: rotate(-75deg);
    box-shadow: 
      3em 2.5em 0 0em var(--mooncolor) inset,
      rgba(255, 255, 255, 0.1) 0em -7em 0 -4.5em,
      rgba(255, 255, 255, 0.1) 3em 7em 0 -4.5em,
      rgba(255, 255, 255, 0.1) 2em 13em 0 -4em,
      rgba(255, 255, 255, 0.1) 6em 2em 0 -4.1em,
      rgba(255, 255, 255, 0.1) 8em 8em 0 -4.5em,
      rgba(255, 255, 255, 0.1) 6em 13em 0 -4.5em,
      rgba(255, 255, 255, 0.1) -4em 7em 0 -4.5em,
      rgba(255, 255, 255, 0.1) -1em 10em 0 -4.5em;
      left:51%;
    } 
      
    &.toggle--nochecked{
      left: 30px;
      top:24px;
      width: 6.5em;
      height: 6.5em;
      background: #fff;
      transform: rotate(0deg);
      box-shadow: 
      0 0 20px 0 white, 
      10px 10px 20px 10px purple,
      -10px - 10px 20px 0 cyan,
      0 0 14px 0 white inset, 
      -40px -30px 60px 0 purple inset, 
      -40px -30px 70px 0 blue inset, 
      -40p -30px 100px 0 cyan inset, 
      -40px -30px 140px 0 powderblue inset;
    }   
  }
`;

const Desc = styled.div`
  // TODO : 설명 부분의 CSS를 구현합니다.
  text-align:center;
  margin-top:10px;
  font-weight: 500;
  > .switch--On{
    color: var(--mint-700);
  }
  > .switch--Off{
    color: var(--peach-400);
  }
`;

export const Toggle = () => {
  const [isOn, setisOn] = useState(false);

  const toggleHandler = () => {
    // TODO : isOn의 상태를 변경하는 메소드를 구현합니다.
    setisOn(!isOn)
  };
  return (
    <Wrap className={`${isOn ? "toggle--wrapbg" : "toggle--nowrapbg"}`}>
      <ToggleContainer
        // TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
        onClick={toggleHandler}
      >
        {/* TODO : 아래에 div 엘리먼트 2개가 있습니다. 각각의 클래스를 'toggle-container', 'toggle-circle' 로 지정하세요. */}
        {/* TIP : Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 div 엘리먼트 2개에 모두 추가합니다. 조건부 스타일링을 활용하세요. */}
        <div className={`toggle-container ${isOn ? "toggle--checked" : "toggle--nochecked"}`}/>
        <div className={`toggle-circle ${isOn ? "toggle--checked" : "toggle--nochecked"}`}></div>
      </ToggleContainer>
      {/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
      {/* TIP:  Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
      {isOn === false ? 
        <Desc><p className='switch--On'>아침이네요</p></Desc>:
        <Desc><p className='switch--Off'>저녁이군요</p></Desc>
      }
    </Wrap>
  );
};

토글UI에서는 isOn 상태를 활용해서 className을 변경해주는 방식으로 CSS를 적용해줘서 토글 스위치가 움직이는 것을 구현할 수 있었다.


Tab 

Tab UI 컴포넌트는 동일한 메뉴 라인에서 뷰를 전환할 때 사용한다. 이 부분에서는 map함수의 두번째 인자로 index를 넣어서 핸들러 함수에 전달해주는 것이 핵심이다. (사실 이 컴포넌트를 구현하면서 map 함수 두번째 인자로 index를 넣어줄 수 있다는 것을 처음 알았다.)

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

// TODO: Styled-Component 라이브러리를 활용해 TabMenu 와 Desc 컴포넌트의 CSS를 구현합니다.
export const Wrapdiv = styled.div`
  display: flex;
  justify-items: center;
  align-items: center;
  flex-direction: column;
`;

export const TabMenu = styled.ul`
  color: rgba(73, 73, 73, 0.5);
  font-weight: bold;
  display: flex;
  flex-direction: row;
  justify-items: center;
  align-items: center;
  list-style: none;
  margin-bottom: 4rem;
  

  .submenu {
    ${'' /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
    width:100px;
    height:40px;
    padding:10px;
    margin: 10px;
    text-align: center;
    border-radius: 17px;  
    background: #e0e0e0;
    box-shadow: 9px 9px 18px #bebebe,
                -9px -9px 18px #ffffff;
    cursor:pointer;
  }

  .focused {
    ${'' /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다.  */}
    border-radius: 17px;
    background: #e0e0e0;
    box-shadow: inset 11px 11px 19px #b3b3b3,
                inset -11px -11px 19px #ffffff;
    color:var(--coz-purple-500);
  }

  & div.desc {
    text-align: center;
  }
`;

const Desc = styled.div`
  text-align: center;
`;

export const Tab = () => {
  // TIP: Tab Menu 중 현재 어떤 Tab이 선택되어 있는지 확인하기 위한
  // currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다.
  const [currentTab, setCurrentTab] = useState(0);

  const menuArr = [
    { name: 'Tab1', content: '첫번째 탭 내용입니다' },
    { name: 'Tab2', content: '두번째 탭 내용 권지용' },
    { name: 'Tab3', content: '세번째 탭 내용 권지용 겟춰크레용' },
  ];

  const selectMenuHandler = (index) => {
    // TIP: parameter로 현재 선택한 인덱스 값을 전달해야 하며, 이벤트 객체(event)는 쓰지 않습니다
    // TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 갱신되도록 함수를 완성하세요.
    setCurrentTab(index);
  };

  return (
      <Wrapdiv>
        <TabMenu>
          {/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
          {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 
                  나머지 2개의 tab은 'submenu' 가 됩니다.*/}
          {/* <li className="submenu">{menuArr[0].name}</li>
          <li className="submenu">{menuArr[1].name}</li>
          <li className="submenu">{menuArr[2].name}</li> */}
          {menuArr.map((menu, index)=>{
            return (<li className={currentTab === index ? "submenu focused" : "submenu"} 
              key={index} onClick={()=>selectMenuHandler(index)}>
                  {menu.name}
            </li>)}
          )}
        </TabMenu>
        <Desc>
          {/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </Wrapdiv>
    
  );
};

Tag

Tag UI 컴포넌트는 레이블 지정을 통해 구성이나 분류에 도움이 되는 키워드 집합을 만들 때 자주 사용된다. input창에 값을 입력하고 Enter키를 누르면 입력이 되어야 하고, 빈값이나 이미 있는 값을 입력하고 Enter를 치면 입력이 되지 않게 구현하여야 한다. 그리고 x버튼을 누르면 삭제도 가능하도록 구현해야 한다. 이 부분에서는 어떻게 하면 입력이 되고 어떨때는 입력이 안되고, 삭제를 구현하는 로직을 짜는 것이 핵심이다. 예전에 twittler에서 tweet을 추가하고 삭제하는 기능을 구현한것과 비슷하게 구현할 수 있었다.

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

// TODO: Styled-Component 라이브러리를 활용해 여러분만의 tag 를 자유롭게 꾸며 보세요!

export const TagsInput = styled.div`
  margin: 8rem auto;
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  min-height: 48px;
  width: 480px;
  padding: 0 8px;
  border: 1px solid rgb(214, 216, 218);
  border-radius: 6px;

  > ul {
    display: flex;
    flex-wrap: wrap;
    padding: 0;
    margin: 8px 0 0 0;

    > .tag {
      width: auto;
      height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      padding: 0 8px;
      font-size: 14px;
      list-style: none;
      border-radius: 6px;
      margin: 0 8px 8px 0;
      background: var(--coz-purple-600);
      > .tag-close-icon {
        display: block;
        width: 16px;
        height: 16px;
        line-height: 16px;
        text-align: center;
        font-size: 14px;
        margin-left: 8px;
        color: var(--coz-purple-600);
        border-radius: 50%;
        background: #fff;
        cursor: pointer;
      }
    }
  }

  > input {
    flex: 1;
    border: none;
    height: 46px;
    font-size: 14px;
    padding: 4px 0 0 0;
    :focus {
      outline: transparent;
    }
  }

  &:focus-within {
    border: 1px solid var(--coz-purple-600);
  }
`;

export const Tag = () => {
  const initialTags = ['CodeStates', 'kimcoding'];

  const [tags, setTags] = useState(initialTags);
  const removeTags = (indexToRemove) => {
    // TODO : 태그를 삭제하는 메소드를 완성하세요.
    setTags([...tags] .filter((v, idx) => idx !== indexToRemove));
  };

  const addTags = (event) => {
    // TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
    // 이 메소드는 태그 추가 외에도 아래 3 가지 기능을 수행할 수 있어야 합니다.
    // - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
    // - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
    // - 태그가 추가되면 input 창 비우기

    if(window.event.keyCode == 13){
        if(event.target.value !== '' && !tags.includes(event.target.value)){
          setTags([...tags, event.target.value]);
        event.target.value = '';
      };
    };
  }

  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
                {/* TODO :  tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
                            삭제 아이콘을 click 했을 때 removeTags 메소드가 실행되어야 합니다. */}
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          onKeyUp={(x) => addTags(x)}
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

addTags 함수안에 보면 input창에 입력된 값을 나타내야한다. 아무것도 없다면 엔터키 입력해도 추가하지 않아야하므로 엔터키를 눌렀을경우 해당 이벤트타겟 값들이 태그에 추가되도록 했다. 엔터키가 입력되야하기에 찾아보다가 keyCode 13이라는걸 찾았다. 근데 이게 좋은 방법은 아닌것 같아서 다시 수정해봐야 할것 같다.


Autocomplete

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

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

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

/* TODO : 아래 CSS를 자유롭게 수정하세요. */
const boxShadow = '0 4px 6px rgb(32 33 36 / 28%)';
const activeBorderRadius = '0 0 1rem 1rem';
const inactiveBorderRadius = '1rem 1rem 1rem 1rem';
export const AutocompleteWrapper = styled.div`
  display: flex;
  justify-content : center;
  align-items : center;
  flex-direction: column;
  
`;
export const InputContainer = styled.div`
  margin-top: 5rem;
  background-color: #ffffff;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding: 0.5rem 1rem;
  /* border: 1px solid rgb(223, 225, 229);
  border-radius: ${inactiveBorderRadius}; */
  border-radius: 11px;
    background: linear-gradient(145deg, #cacaca, #f0f0f0);
    box-shadow:  9px 9px 18px #b3b3b3,
                -9px -9px 18px #ffffff;
  z-index: 3;
  width: 200px;
  &:focus-within {
    border-radius: 11px;
    background: linear-gradient(145deg, #cacaca, #f0f0f0);
    box-shadow:  9px 9px 18px #b3b3b3,
                -9px -9px 18px #ffffff;
  }

  > input {
    width: 150px;
    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;
  width: 200px;
  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: 5px;
  padding: 0.5rem 0;
  /* border: 1px solid rgb(223, 225, 229);
  border-radius:${activeBorderRadius};
  box-shadow: ${boxShadow}; */
  border-radius: 11px;
background: linear-gradient(145deg, #cacaca, #f0f0f0);
box-shadow:  9px 9px 18px #b3b3b3,
             -9px -9px 18px #ffffff;
  z-index: 3;

  > li {
    padding: 0 1rem;
  }
  > li:hover,
  > li.selected {
  background-color: yellow;
  }
`;

export const Autocomplete = () => {
  /**
   * Autocomplete 컴포넌트는 아래 3가지 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
   * - hasText state는 input값의 유무를 확인할 수 있습니다.
   * - inputValue state는 input값의 상태를 확인할 수 있습니다.
   * - options state는 input값을 포함하는 autocomplete 추천 항목 리스트를 확인할 수 있습니다.
   */
  const [hasText, setHasText] = useState(false);
  const [inputValue, setInputValue] = useState('');
  const [options, setOptions] = useState(deselectedOptions);
  const [selected, setSelected] = useState(-1);
  // useEffect를 아래와 같이 활용할 수도 있습니다.
  useEffect(() => {
    if (inputValue === '') {
      setHasText(false);
    }
  }, [inputValue]);

  // TODO : input과 dropdown 상태 관리를 위한 handler가 있어야 합니다.
  const handleInputChange = (event) => {
    /**
     * handleInputChange 함수는
     * - input값 변경 시 발생되는 change 이벤트 핸들러입니다.
     * - input값과 상태를 연결시킬 수 있게 controlled component로 만들 수 있고
     * - autocomplete 추천 항목이 dropdown으로 시시각각 변화되어 보여질 수 있도록 상태를 변경합니다.
     *
     * handleInputChange 함수를 완성하여 아래 3가지 기능을 구현합니다.
     *
     * onChange 이벤트 발생 시
     * 1. input값 상태인 inputValue가 적절하게 변경되어야 합니다.
     * 2. input값 유무 상태인 hasText가 적절하게 변경되어야 합니다.
     * 3. autocomplete 추천 항목인 options의 상태가 적절하게 변경되어야 합니다.
     * Tip : options의 상태에 따라 dropdown으로 보여지는 항목이 달라집니다.
     */
    setInputValue(event.target.value);
    setHasText(!hasText);
    setOptions(deselectedOptions.filter(el=>el.includes(event.target.value)))
  };

  const handleDropDownClick = (clickedOption) => {
    /**
     * handleDropDownClick 함수는
     * - autocomplete 추천 항목을 클릭할 때 발생되는 click 이벤트 핸들러입니다.
     * - dropdown에 제시된 항목을 눌렀을 때, input값이 해당 항목의 값으로 변경되는 기능을 수행합니다.
     *
     * handleInputChange 함수를 완성하여 아래 기능을 구현합니다.
     *
     * onClick 이벤트 발생 시
     * 1. input값 상태인 inputValue가 적절하게 변경되어야 합니다.
     * 2. autocomplete 추천 항목인 options의 상태가 적절하게 변경되어야 합니다.
     */
    const newOption = [clickedOption]
    setInputValue(clickedOption);
    setOptions(newOption);
  };

  const handleDeleteButtonClick = () => {
    /**
     * handleDeleteButtonClick 함수는
     * - input의 오른쪽에 있는 X버튼 클릭 시 발생되는 click 이벤트 핸들러입니다.
     * - 함수 작성을 완료하여 input값을 한 번에 삭제하는 기능을 구현합니다.
     *
     * handleDeleteButtonClick 함수를 완성하여 아래 기능을 구현합니다.
     *
     * onClick 이벤트 발생 시
     * 1. input값 상태인 inputValue가 빈 문자열이 되어야 합니다.
     */
    setInputValue('');
  };

  // Advanced Challenge: 상하 화살표 키 입력 시 dropdown 항목을 선택하고, Enter 키 입력 시 input값을 선택된 dropdown 항목의 값으로 변경하는 handleKeyUp 함수를 만들고,
  // 적절한 컴포넌트에 onKeyUp 핸들러를 할당합니다. state가 추가로 필요한지 고민하고, 필요 시 state를 추가하여 제작하세요.
  const handleKeyUp = (event) => {
    if (event.code === "ArrowUp" && selected > 0) {
      setSelected(selected - 1);
      setInputValue(options[selected - 1]);
    }
    if (event.code === "ArrowDown" && 
        selected < options.length - 1 && 
        event.nativeEvent.isComposing === false
      ){
      setSelected(selected + 1);
      setInputValue(options[selected + 1]);
    }
    if (event.code === "Enter" && selected >= 0) {
      handleDropDownClick(options[selected]);
      setSelected(-1);
    }
  }
  
  

  return (
    // <div className='autocomplete-wrapper'>
   
    <AutocompleteWrapper>
      <InputContainer>
        {/* TODO : input 엘리먼트를 작성하고 input값(value)을 state와 연결합니다. handleInputChange 함수와 input값 변경 시 호출될 수 있게 연결합니다. */}
        <input 
            type="text" 
            value={inputValue} 
            onChange={handleInputChange} 
            onKeyDown={handleKeyUp}  
            placeholder="검색어 입력">
        </input>
        {/* TODO : 아래 div.delete-button 버튼을 누르면 input 값이 삭제되어 dropdown이 없어지는 handler 함수를 작성합니다. */}
        <div className='delete-button' onClick={handleDeleteButtonClick}>
            &times;
        </div>
      </InputContainer>
      {/* TODO : input 값이 없으면 dropdown이 보이지 않아야 합니다. 조건부 렌더링을 이용해서 구현하세요. */}
      {hasText ? (
        <DropDown 
          options={options} 
          handleComboBox={handleDropDownClick} 
          selected={selected}/>
      ) : (
        []
      )}
    {/* </div> */}
    </AutocompleteWrapper>
  );
};

// export const DropDown = ({ options, handleComboBox}) => {
//   return (
//     <DropDownContainer>
//       {/* TODO : input 값에 맞는 autocomplete 선택 옵션이 보여지는 역할을 합니다. */}
//       {options.map((el, idx)=>{
//         return <li key={idx}  onClick={()=> handleComboBox(el)} >{el}</li>
//       })}
//     </DropDownContainer>
//   );
// };

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

options를 변경해주는 부분을 useEffect에서 관리하니까 그냥 inputValue가 변할때 마다 알아서 options가 바뀌다보니 편하게 관리해줄 수 있었다. useEffect에서 관리를 해주지 않았다면 inputValue를 변하게 해주는 함수마다 options도 변하게 해주는 함수를 구현해주었어야 할 것이다. 또 키보드 이벤트 발생시 처음 아래로 내릴경우 2번 이동하는 현상이 발생하기에 isComposing 메서드를 찾아서 추가해주었다. 


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%;
`
export const InputBox = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  /* text-align: center; */
  /* display: inline-block; */
  width: 150px;
  height: 30px;
  border-radius: 10px;
  margin-left: 1rem;  background-color: #fff;
`;

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

`;

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

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

export const MyInput = ({ value, handleValueChange }) => {
  const inputEl = useRef(null);
  const [isEditMode, setEditMode] = useState(false);
  const [newValue, setNewValue] = useState(value);

  useEffect(() => {
    if (isEditMode) {
      inputEl.current.focus();
    }
  }, [isEditMode]);

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

  const handleClick = () => {
    // TODO : isEditMode 상태를 변경합니다.
    setEditMode(!isEditMode)
  };

  const handleBlur = () => {
    // TODO : Edit가 불가능한 상태로 변경합니다.
    setEditMode(false)
    handleValueChange(newValue);
  };

  const handleInputChange = (e) => {
    // TODO : 저장된 value를 업데이트합니다.
    setNewValue(e.target.value)
  };

  return (
    <InputBox>
      {isEditMode ? (
        <InputEdit
          type='text'
          value={newValue}
          ref={inputEl}
          // TODO : 포커스를 잃으면 Edit가 불가능한 상태로 변경되는 메소드가 실행되어야 합니다.
          onBlur={handleBlur}
          // TODO : 변경 사항이 감지되면 저장된 value를 업데이트 되는 메소드가 실행되어야 합니다.
          onChange={handleInputChange}
        />
      ) : (
        <span 
        // TODO : 클릭하면 Edit가 가능한 상태로 변경되어야 합니다.
        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 (
    <>
      <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>
    </>
  );
};

알게된 것 간단 정리

- focus와 blur

focus와 blur는 자바스크립트에서 DOM 요소에 대한 이벤트 처리를 할 때 사용되는 메소드다.

focus는 요소에 포커스가 이동했을 때 발생한다. input 요소에 마우스를 클릭하여 포커스가 이동하거나, Tab 키를 이용하여 요소에 포커스가 이동할 때 발생하고 focus 이벤트는 보통 요소에 스타일을 적용하거나, 입력값 검사 등과 같은 처리를 할 때 사용한다.

blur는 요소에서 포커스가 벗어났을 때 발생하는 이벤트다.  input 요소에서 마우스를 클릭하여 포커스가 이동한 후, 다른 곳을 클릭하거나 다른 요소로 포커스가 이동할 때 발생한다. blur 이벤트는 입력값 검사나 포커스 이벤트 등과 같은 처리를 할 때 사용한다.

focus와 blur 이벤트는 모두 addEventListener() 메소드를 사용하여 등록할 수 있다. 예를 들어, 아래의 코드는 id가 "my-input"인 input 요소에서 focus 이벤트가 발생했을 때, 콘솔에 "Input에 포커스가 갔습니다."라는 메시지를 출력하도록 등록하는 코드다.

const inputEl = document.getElementById("my-input");

inputEl.addEventListener("focus", function() {
  console.log("Input에 포커스가 갔습니다.");
});

- useRef

이전 블로깅 참고

 

useRef

useRef는 더욱 이해가 잘 가지않는다. 재생목록을 다 듣고 시작해보자. 유투브 별코딩 useRef useRef 란? useRef는 React Hooks API 중 하나로, 함수형 컴포넌트에서 DOM 요소에 직접 접근하거나, 컴포넌트 내

gjy0605.tistory.com

- stopPropagation

topPropagation은 자바스크립트에서 이벤트 버블링을 막을 때 사용하는 메소드다

이벤트 버블링은 HTML 요소에서 이벤트가 발생한 후, 해당 요소의 부모 요소에서도 이벤트가 발생하는 현상을 의미한다. 예를 들어, 아래와 같은 HTML 구조가 있다고 보면

<div id="parent">
  <div id="child">
    <button id="button">Click me!</button>
  </div>
</div>

만약 button 요소에서 클릭 이벤트가 발생하면, 이벤트는 button -> child -> parent 순서로 진행된다. 이러한 이벤트 버블링은 때로는 원하지 않는 결과가 나올 때도 있다. 가령, child 요소에서 클릭 이벤트를 처리하고 싶지만, parent 요소에서도 클릭 이벤트가 발생하면 처리를 할 수 없다.

이때, stopPropagation 메소드를 사용하면 이벤트 버블링을 막을 수 있다.  child 요소에서 버튼 클릭 이벤트가 발생할 때, 이벤트 전달을 막으려면 아래와 같이 코드를 작성할 수 있습니다.

const childEl = document.getElementById("child");

childEl.addEventListener("click", function(event) {
  event.stopPropagation();
  console.log("Child 요소에서 클릭 이벤트 처리");
});

위 코드에서 stopPropagation() 메소드는 클릭 이벤트가 child 요소에서 처리되고 난 후, 이벤트 전달을 막는다. 따라서, 버튼을 클릭하더라도 parent 요소에서 클릭 이벤트가 발생하지 않는다.

- isComposing

isComposing은 자바스크립트에서 입력 이벤트를 처리할 때 사용되는 속성 중 하나다.

isComposing 속성은 사용자가 글자를 입력하고 있는지 여부를 나타내며, 일본어나 한글과 같이 입력 중인 글자가 완성되지 않은 경우에 true 값을 반환한다. 이 속성은 보통 compositionstart, compositionupdate, compositionend와 함께 사용된다.

compositionstart 이벤트는 입력 중인 글자가 시작될 때, compositionupdate 이벤트는 입력 중인 글자가 변경될 때, compositionend 이벤트는 입력 중인 글자가 완료될 때 발생한다. 이때, isComposing 속성을 사용하여 입력 중인 글자가 완성되었는지 여부를 확인할 수 있다.

아래의 예시는 input 요소에서 입력 중인 글자가 완료되었는지 확인한다.

<input type="text" id="my-input">
const inputEl = document.getElementById("my-input");

inputEl.addEventListener("compositionend", function(event) {
  if (!event.target.value.isComposing) {
    console.log("입력 중인 글자가 완료되었습니다.");
  }
});

코드에서 event.target.value.isComposing 속성을 사용하여 입력 중인 글자가 완료되었는지 여부를 확인하고 있다. 만약, isComposing 속성이 false 값을 반환하면 "입력 중인 글자가 완료되었습니다."라는 메시지를 출력한다.

'프론트엔드 공부 > React' 카테고리의 다른 글

Redux  (0) 2023.02.24
상태 관리  (1) 2023.02.24
useRef  (0) 2023.02.22
[React] Custom Component 종합퀴즈  (0) 2023.02.22
Styled Components  (0) 2023.02.20
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.