Front End/React

UI 디자인 패턴 구현

Modal

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

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

  const openModalHandler = (event) => {
    event.preventDefault();
    setIsOpen(!isOpen);
  };

  return (
    <>
      <ModalContainer>
        <ModalBtn onClick={openModalHandler}>
          {isOpen ? "Opened!" : "Open Modal"}
        </ModalBtn>
        {isOpen ? (
          <ModalBackdrop
            ref={refBackdrop}
            onClick={(e) => {
              if (e.target === refBackdrop.current) openModalHandler(e);
            }}
          >
            <ModalView>
              <p>Hello World!</p>
            </ModalView>
          </ModalBackdrop>
        ) : undefined}
      </ModalContainer>
    </>
  );
};

기능 구현

  • 필요한 state
    • 창이 떠있는지 아닌지를 판단하는 불리언 값을 가진 상태
  • 삼항연산자를 이용해 state 값에 따라 모달창을 보여주거나 숨김
  • 모달의 배경을 클릭했을 때 모달창이 꺼지는 것을 구현하기 위해서 버튼에 적용한 이벤트(onClick)를 배경에도 동일하게 작성
    • 이때, 이벤트 버블링(부모요소에 적용된 이벤트가 자식에게도 적용되는 현상)을 막기 위한 방법은 여러가지가 있다.
      1. useRef를 사용해 배경의 DOM 주소에 접근하여 판별 (비추천)
        <ModalBackdrop
        	ref={refBackdrop}
        	onClick={(e) => {
        	  if (e.target === refBackdrop.current) openModalHandler(e);
        	  }
        	}
        >
      1. HTML에서 사용되는 접근성을 위한 속성인 role을 이용해 event.target에 접근하여 판별
        export const ModalView = styled.div.attrs((props) => ({
          role: "dialog",
        })
        
        // 중략
        
        <ModalBackdrop
        	onClick={(e) => {
            if (e.target.role !== 'dialog') openModalHandler(e);
            }
        	}
        >
      1. 이벤트 버블링을 막기 위해 기본적으로 제공되는 이벤트 객체 메서드 stopPropagation()를 모달창에 적용
        <ModalBackdrop
        	onClick={(e) => openModalHandler(e)}}
        >
        	<ModalView onClick={(e) => e.stopPropagation()}>

Tab

export const Tab = () => {
  const [currentTab, setCurrentTab] = useState(0);

  const menuArr = [
    { name: "Tab1", content: "Tab menu ONE" },
    { name: "Tab2", content: "Tab menu TWO" },
    { name: "Tab3", content: "Tab menu THREE" },
  ];

  const selectMenuHandler = (index) => {
    setCurrentTab(index);
  };

  return (
    <>
      <div>
        <TabMenu>
          {menuArr.map((el, index) => {
            return (
              <li
                key={el.name}
                className={currentTab === index ? "submenu focused" : "submenu"}
                onClick={() => selectMenuHandler(index)}
              >
                {el.name}
              </li>
            );
          })}
        </TabMenu>
        <Desc>
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

기능 구현

  • 필요한 state
    • 현재 탭을 확인하고 렌더 할 넘버 값(index)을 가진 변수
  • 탭으로 표시 해야 하는 내용들을 배열 형태로 작성하거나 받아오고, .map을 이용해 탭 메뉴를 출력
  • 클릭 이벤트 핸들러에는 해당 li의 index를 받아 state를 변경하는 함수 작성
  • 현재 탭의 상태를 표시 할 클래스 스타일을 작성하고 state와 렌더된 index를 비교하여 클래스를 추가/제거하는 삼항연산자 작성

Tag

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

  const [tags, setTags] = useState(initialTags);
  const removeTags = (indexToRemove) => {
    setTags((prev) => prev.filter((el, index) => index !== indexToRemove));
  };

  const addTags = (event) => {
    setTags((prev) => prev.concat(event));
  };

  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);
                }}
              >
                &times;
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          onKeyUp={(e) => {
            if (
              e.key === "Enter" &&
              e.target.value !== "" &&
              !tags.includes(e.target.value)
            ) {
              addTags(e.target.value);
              e.target.value = "";
              console.log("실행");
            }
          }}
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

기능 구현

  • 필요한 state
    • 현재 태그를 표시 하여 스트링 값을 배열의 형태로 저장하는 상태
  • 태그 추가는 입력창의 value를 받아서 state에 추가하는 함수를 작성하고 키 업 이벤트를 통해 상태 변경 함수를 실행 → 이 때, 입력창에 value가 없거나 이미 존재하는 태그 일 경우를 조건을 통해 동작하지 않게 할 수 있음
  • 태그 제거는 .map으로 렌더된 li의 index를 인자로 받아 state 내 값을 제거하는 함수를 작성
    ⚠️
    배열 형태의 state를 변경할 때는 불변성 법칙을 지켜 원본 배열을 변경하는 것이 아닌 새로운 배열을 반환하는 형태의 메서드를 사용해야 한다. (filter, concat 등)

Toggle

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

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

  const toggleHandler = () => {
    setisOn(!isOn);
  };

  return (
    <>
      <ToggleContainer onClick={toggleHandler}
        // TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
      >
        <div className={isOn ? 'toggle-container toggle--checked' : 'toggle-container'}/>
        <div className={isOn ? 'toggle-circle toggle--checked' : 'toggle-circle'}/>
      </ToggleContainer>
      <Desc>
        {isOn ? 'Toggle Switch ON' : 'Toggle Switch OFF'}
      </Desc>
    </>
  );
};

기능 구현

  • 필요한 state
    • 토글의 현재 상태를 판단하는 불리언 값을 가진 상태
  • 클릭을 하면 상태를 변경하는 함수 작성 후 이벤트 핸들러로 연결
  • 토글의 상태에 따라 스타일이 변할 수 있게 삼항연산자로 클래스명 제어
    • 추가되는 클래스에 transition 속성을 부여 해 자연스러운 동작 구현 가능

Autocomplete

export const Autocomplete = () => {
  const [hasText, setHasText] = useState(false);
  const [inputValue, setInputValue] = useState("");
  const [options, setOptions] = useState([]);
  const [opIndex, setOpIndex] = useState(-1);

  useEffect(() => {
    if (inputValue === "") {
      setHasText(false);
    } else {
      setHasText(true);
      setOptions(deselectedOptions.filter(el => el.indexOf(inputValue) >= 0))
    }
  }, [inputValue]);

  const handleInputChange = (event) => {
    setInputValue(event);
  };

  const handleDropDownClick = (clickedOption) => {
    setInputValue(clickedOption);
  };

  const handleDeleteButtonClick = () => {
    setInputValue("");
  };

  const handleKeyUp = (event) => {
    if(event === 'ArrowUp') setOpIndex(prev => {
      if(prev >= 0) return prev - 1;
      else return prev;
    })
    else if(event === 'ArrowDown') setOpIndex(prev => {
      if(prev < options.length - 1) return prev + 1;
      else return prev;
    })
    else if(event === 'Enter') {
      handleDropDownClick(options[opIndex]);
      setOpIndex(prev => prev = -1);
    }
  }

  return (
    <div className="autocomplete-wrapper">
      <InputContainer hasText={hasText}>
        <input
          type="text"
          value={inputValue}
          onChange={(event) => handleInputChange(event.target.value)}
          onKeyUp={(event) => handleKeyUp(event.code)}
        />
        <div className="delete-button" onClick={handleDeleteButtonClick}>
          &times;
        </div>
      </InputContainer>
      {hasText ? <DropDown options={options} opIndex={opIndex} handleComboBox={handleDropDownClick}/> : undefined}
    </div>
  );
};

export const DropDown = ({ options, opIndex, handleComboBox }) => {
  return (
    <DropDownContainer>
      {options.map((el, index) => (
        <li className={opIndex === index ? 'active' : ''} key={index} onClick={() => handleComboBox(el)} >{el}</li>
      ))}
    </DropDownContainer>
  );
};

기능 구현

  • 필요한 state
    • hasText : input 내에 값이 존재하는지 판단하는 불리언 값의 상태
    • inputValue : input의 value 값을 확인하고 저장하는 스트링 값의 상태
    • options : 자동완성 추천 값을 배열의 형태로 가진 상태
    • opIndex : (Advanced) 자동 완성 추천 값을 키보드로 조작하기 위한 index 값을 가진 상태
  • 자동완성 컴포넌트 자체는 hasText를 이용해 삼항연산자를 활용해 렌더하고, 내부에는 options.map을 이용해 렌더
    • 해당 요소를 클릭했을 때 그 값이 input에 들어갈 수 있도록 클릭 이벤트 핸들러 작성
  • 자동 완성은 inputValue가 변경될 때 자동완성의 원본 데이터를 filter를 이용해 options를 변경
  • 방향키를 이용한 제어는 키 업 이벤트를 이용해 각각의 방향키에 opIndex 상태를 변경하는 기능과 엔터를 입력하면 해당 요소가 input에 들어갈 수 있도록 inputValue를 변경하는 함수를 작성해 연결하고, 선택 된 추천 요소를 볼 수 있게 클래스에 스타일을 작성하여 index에 맞춰서 클래스 변경.


Uploaded by N2T