41일차 React Custom Component

2021. 9. 16. 00:20
반응형

2021. 09. 14 화요일

1. Today's Key Points!🔑

  • React Custom Component
  • Modal
  • Toggle
  • Tab
  • Tag

2. Sprint과제 복기!🧐

👉🏻Modal Component

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

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

export const ModalContainer = styled.div`
  display : flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  position: relative;
`;

export const ModalBackdrop = styled.div`
  position: fixed;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 1;
`;

export const ModalBtn = styled.button`
  background-color: #4000c7;
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
  cursor: grab;
`;

export const ModalView = styled.div.attrs(props => ({
  role: 'dialog'
}))`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: white;
  width: 200px;
  height: 100px;
  border-radius: 1rem;
  position: relative;
  >.close-btn{
    position: absolute;
    top:2px;
    right:7px;
    cursor: pointer;
  }
`;

export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false); //isOpen 상태를 만들어준다.
  const openModalHandler = (event) => {
    setIsOpen(!isOpen);
  };

  return (
    <>
      <ModalContainer>
        <ModalBtn onClick={openModalHandler}>
          {isOpen ? 'Opened!' : 'Open Modal'} //모달창이 열렸을 때 버튼은'Opened!' 닫혔을 때 'Open Modal'
        </ModalBtn>
        //isOpen이 true이면 모달창을 열고, false이면 null값을 준다.
        {isOpen ? <ModalBackdrop onClick={openModalHandler}>
                    <ModalView onClick={(event) => {event.stopPropagation()}}>
                      <div className="close-btn" onClick={openModalHandler}>&times;</div>
                      <div>HELLO CODESTATES!</div>
                    </ModalView>
                  </ModalBackdrop> : null}
      </ModalContainer>
    </>
  );
};

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

👉🏻Toggle Component

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

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

const ToggleContainer = styled.div`
  position: relative;
  margin-top: 8rem;
  left: 47%;
  cursor: pointer;

  > .toggle-container {
    width: 50px;
    height: 24px;
    border-radius: 30px;

    background-position: right;
      background: linear-gradient(to left, #8b8b8b 50%, blue 50%) right;
      background-size: 200%;
      transition: 1s;
    &.toggle--checked{
      background-position: left;
      background: linear-gradient(to right, blue 50%, #8b8b8b 50%) left;
      background-size: 200%;
      transition: 1s;
    }
  }

  > .toggle-circle {
    position: absolute;
    top: 1px;
    left: 1px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background-color: #ffffff;
    transition: 1s;
    &.toggle--checked {
    left: 27px;
    transition: 1s;
    }
  }
`;

const Desc = styled.div`
  display: flex;
  justify-content: center;
  margin-top: 0.5rem;
`;

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

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

  return (
    <>
      <ToggleContainer onClick={toggleHandler}>
        <div className={`toggle-container ${isOn ? 'toggle--checked' : ''}`}/>
        <div className={`toggle-circle ${isOn ? 'toggle--checked' : ''}`}/>
      </ToggleContainer>
      <Desc><div>{isOn ? 'Toggle Switch ON' : 'Toggle Switch OFF'}</div></Desc>
    </>
  );
};

linear-gradient를 사용하면 배경이 한쪽에서 부터 점점 채워지는 느낌으로 구현할 수 있다. 그리고 토글UI에서는 isOn 상태를 활용해서 className을 변경해주는 방식으로 CSS를 적용해줘서 토글 스위치가 움직이는 것을 구현할 수 있었다.

Toggle 구현 결과

👉🏻Tab Component

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

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

const TabMenu = styled.ul`
  background-color: #dcdcdc;
  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: 7rem;
  height: 40px;

  .submenu {
    display: flex;
    justify-content: center;
    flex-grow: 1;
    cursor: pointer;
  }

  .focused {
    background-color: blue;
    color: white;
    height: 100%;
    display: flex;
    align-items: center;
    transition: 1s;
  }

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

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

export const Tab = () => {

  const menuArr = [
    { name: 'Tab1', content: 'Tab menu ONE' },
    { name: 'Tab2', content: 'Tab menu TWO' },
    { name: 'Tab3', content: 'Tab menu THREE' },
  ];
  const [currentTab, setCurrentTab] = useState(0)

  const selectMenuHandler = (index) => {
    console.log(index);
    setCurrentTab(index);
  };

  return (
    <>
      <div>
        <TabMenu>
          {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 
                  나머지 2개의 tab은 'submenu' 가 된다.*/}
          {menuArr.map((el, index) => {
            return <li key={index} 
                       className={`${index === currentTab ? 'submenu focused' : 'submenu'}`}
                       onClick={() => selectMenuHandler(index)}>{el.name}</li>
          })}
        </TabMenu>
        <Desc>
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

li 엘리먼트를 map 함수로 menu의 갯수만큼 만들어준다. 이때 인덱스를 두번째 인자로 넣어서 onClick 핸들러 함수에 index를 전달해준다. 그리고 인덱스를 전달받은 함수를 통해서 currentTab 상태를 해당 인덱스로 바꿔주면서 클릭된 Tab의 className을 바꿔줌으로 인해서 클릭 된 메뉴의 CSS 속성만 바꿔 선택되었음을 시각화 시켜줄 수 있게된다.

👉🏻Tag Component

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

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

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: #4000c7;
        > .tag-close-icon {
        display: block;
        width: 16px;
        height: 16px;
        line-height: 16px;
        text-align: center;
        font-size: 14px;
        margin-left: 8px;
        color: #4000c7;
        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 #4000c7;
  }

`;

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

  const [tags, setTags] = useState(initialTags);

  const removeTags = (indexToRemove) => { //삭제기능 구현
  //여기서도 map함수의 index를 전달받아 클릭된 인덱스 정보를 활용해서 삭제를 시킬 수 있다.
    setTags(tags.filter((tag) =>{
      return tag !== tags[indexToRemove]
    }));
  };
  
  const addTags = (event) => { //tag추가 기능 구현
    let value = event.target.value.trim();
    // 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
    // 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
    if(event.key === 'Enter' && !tags.includes(value) && value){
      setTags([...tags, value]);
      // 태그가 추가되면 input 창 비우기
      event.target.value ="";
    }
    else if(event.key === 'Enter' && !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)}>&times;
              </span>
            </li>
          ))}
        </ul>
        <input
          className='tag-input'
          type='text'
          onKeyUp={(event) => {addTags(event)}}
          placeholder='Press enter to add tags'
        />
      </TagsInput>
    </>
  );
};

addTags 함수안에 보면 input창에 입력된 값을 trim 해주고있는데, trim해준 이유가 무엇일까? 공백만 입력한 경우에도 입력이 되지않게 해주기 위함이다. trim()의 기능을 살짝 보자면 예를들어, let str = '       Hello    '와 같은 str이 있다고 가정해보자. 이것을 str.trim() 해주게되면 양쪽의 빈칸이 다 사라지고 'Hello'만 남게해주는 메소드이다. 그렇기 때문에 tag 입력창에 '       '를 입력해주려고 하면 trim이 되서 value는 ''이 되고, 빈값은 입력이 안되게 구현했기 때문에 결국 '       '도 입력이 안되게 된다. 

Tag 구현 결과

 

반응형
LIST

BELATED ARTICLES

more