[06.20] React Custom Component

0
post-thumbnail

React Custom Component

과제 개요

  • Modal, Toggle, Tab, Autocomplete, ClickToEdit, Tag UI 컴포넌트 만들어보기
  • UI 컴포넌트들을 개발하며 Storybook을 사용해서 UI들의 작동 방법 미리보기

파일 구조

/
├── /React Custom Component
│   ├── README.md
│   ├── /public               # create-react-app이 만들어낸 폴더로 yarn/npm start로 실행 시에 쓰입니다
│   └── /src
│        ├── /components      # 단일 UI React 컴포넌트가 들어가는 폴더
│        ├─── /__test__                 # 테스트 케이스가 들어가는 폴더
│        ├─── /AdvancedChallenges       # Advanced Challenges 를 위한 폴더
│        ├─── /BareMinimumRequirements  # Bare Minimum Requirements 를 위한 폴더
│        ├── /stories         # Storybook이 작동하는 데 필요한 파일들이 들어가는 폴더
│        ├── app.css
│        ├── App.js           # React Custom Component App이 작성되어 있습니다.
│        ├── index.js
├  package.json
└ .gitignore

Modal.js

📍 새로 알게된 내용

  • 이벤트 전파

    • 버블링(Bubbling) : 자식 요소에서 발생한 이벤트가 바깥 부모 요소로 전파
    • 캡쳐링(Capturing) : 자식 요소에서 발생한 이벤트가 부모 요소로부터 시작하여 안쪽 자식 요소까지 도달

    -> 버블링 효과를 막아주고 싶은 위치에 event객체를 사용해서 stopPropagation()를 사용

  • X 버튼 구현방법 HTML Entites 사용 (참고)

[전체 코드]

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

export const ModalContainer = styled.div`
// TODO : Modal을 구현하는데 전체적으로 필요한 CSS를 구현합니다.
display: flex;
justify-content: center;
align-items: center;
// 부모요소만큼 높이를 줘야지 세로 가운데 정렬이 됨
height: 100%;
width: 88%;
`;

export const ModalBackdrop = styled.div`
// TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
position: absolute;
//rgb(0,0,0)=black
background-color: rgba(0,0,0,0.3);
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
height: 43%;
width: 89%;
/* left, right, bottom, top : 0;으로 값넣어주면 화면전체에 꽉차게 됨 */
`;

export const ModalBtn = styled.button`
background-color: var(--coz-purple-600);
text-decoration: none;
border: none;
padding: 20px;
color: white;
border-radius: 30px;
cursor: grab;
`;

export const ModalView = styled.div.attrs((props) => ({
// attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
role: 'dialog',
}))`
// TODO : Modal창 CSS를 구현합니다.
display: flex;
align-items: center;
flex-direction: column;
background-color: white;
height: 30%;
width: 20%;
border-radius: 10px;
> .exitbtn {
  margin : 5px 0px 15px 0px;
}
.Modal{
font-size: 20px;
}
`;

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

const openModalHandler = () => {
  // isOpen의 상태를 변경하는 메소드를 구현
  // 초기값이 false기 때문에 !를 붙여서 반대의 상태(true)가 되게끝 해주기위해 전달인자로 !isOpen을 사용
  setIsOpen(!isOpen);
};

return (
  <>
    <ModalContainer>
      {/* ModalBtn 컴포넌트*/}
      <ModalBtn onClick={openModalHandler}>
        {/* Modal이 열린 상태일때는 opend로 닫혔을대는 open modal로 구현 */}
        {isOpen === true ? 'Opened!' : 'Open Modal'}
      </ModalBtn>
        {/* ModalBackdrop 컴포넌트
        // modal이 열린상태일때만 모달창과 배경이 나타나게 구현 */}
        {isOpen === true ? 
        <ModalBackdrop onClick={openModalHandler}>
          {/* 자식 컴포넌트(x 버튼)에 이벤트 핸들러를 만들었을때 부모 컴포넌트(모달)에도 같은 핸들러 작동되는 것을 방지하기 위해(:이벤트가 점차위로 퍼지는 이벤트 버블링 현상이라함)
          stopPropagation()를 사용
          모달 창이 떴을때 배경을 클릭하면 모달 창이 종료되지만 모달 창 내부를 클릭하면 아무일도 일어나지 않게 하기 위해 해당 메서드 사용 */}
          // 버블링을 막아주고 싶은 위치에서 event객체를 사용함
          <ModalView onClick={(event) => {
            event.stopPropagation()
          }}>
          {/* $times는 x버튼 표시 방법 */}
          <div className = 'exitbtn' onClick={openModalHandler}>&times;</div>
          <div className= 'Modal'> Hello Codestates! </div>
          </ModalView>
        </ModalBackdrop> : null}
    </ModalContainer>
  </>
);
};

Toggle.js

📍 새로 알게된 내용

  • 템플릿 리터럴과 삼항 연산자를 활요해 조건부 스타일링 적용시키기
    -> Toggle 스위치가 on인 상태인 경우에 toggle-containertoggle-circletoggle--checked이고 아니면 toggle--nonchecked로 적용
    <div className={`toggle-container ${isOn ? "toggle--checked" : "toggle--nonchecked"}`} />

[전체 코드]

 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-color: #8b8b8b;
   // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
 }
 > .toggle--checked {
   //toggle 스위치가 on이 되었을때 컨테이너의 배경색
   background-color: #4942E4;
 } 
 .toggle--nonchecked {
   //toggle 스위치가 off가 되었을때 컨테이너의 배경색
   background-color: #9BA4B5;
 }

 > .toggle-circle {
   position: absolute;
   top: 1px;
   left: 1px;
   width: 22px;
   height: 22px;
   border-radius: 50%;
   background-color: #ffffff;
   // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
 }
 > .toggle--checked {
   // toggle-circle이 on이 되었을때 변화를 주는 메서드 (transition)
   transition: 1s;
   left: 27px;
 } 
 .toggle--nonchecked {
   // toggle-circle이 off가 되었을때 변화를 주는 메서드 (transition)
   transition: 1s;
 }
`;

const Desc = styled.div`
 // TODO : 설명 부분의 CSS를 구현합니다.
 display: flex;
 justify-content: center;
`;

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

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

 return (
   <>
     <ToggleContainer
       // TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
       onClick={toggleHandler}
     >
       {/* TIP : Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 
       div 엘리먼트 2개에 모두 추가합니다. 조건부 스타일링을 활용하세요. */}
       <div className={`toggle-container ${isOn === true ? "toggle--checked" : "toggle--nonchecked"}`}/>
       <div className={`toggle-circle ${isOn === true ? "toggle--checked" : "toggle--nonchecked"}`}/>
     </ToggleContainer>
     {/* desc 컴포넌트는 스위치가 켜졌을때(isOn===true)와 꺼졌을때(isOn===false)에 나타나는 텍스트를 조건부 렌더링으로 구현  */}
     <Desc>
       {isOn ? "Toggle Switch ON" : "Toggle Switch OFF"}
     </Desc>
   </>
 );
};

Tab.js

📍 새로 알게된 내용

  • 이벤트 핸들러 함수 사용할때
    • 함수 자체 호출 : 함수 내부에 값이 있는 경우
    • 콜백함수를 사용 : 함수 외부에 값이 있는 경우
    {menuArr.map((el, index) => {
    return <li key = {index} 
    className={`${currentTab === index ? "submenu focused" : "submenu"}`} 
    // 리액트 이벤트 핸들러 함수에서는 콜백함수로 전달을 줘야함
    // index의 값을 받아와야 하기 때문에 콜백함수로 사용해야함 
    onClick={() => selectMenuHandler(index)}>{el.name}</li>})}

[전체코드]

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

// TODO: Styled-Component 라이브러리를 활용해 TabMenu 와 Desc 컴포넌트의 CSS를 구현합니다.

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: 7rem;
cursor: pointer;

.submenu {
  ${'' /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
  background-color: #F1F6F9;
  height: 50px;
  width: calc(100% / 3);
  /* 오른쪽 상단만 효과주기 */
  margin-top: 10px;
  display: flex;
  // 수직 중앙 정렬
  align-items: center;
  // 수평 중앙 정렬
  justify-content: center;
}

.focused {
  ${'' /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다.  */}
  &:hover{
    background-color: #9BA4B5;
    transition: 0.5s;
  }
}

& 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: 'Tab menu ONE' },
  { name: 'Tab2', content: 'Tab menu TWO' },
  { name: 'Tab3', content: 'Tab menu THREE' },
];

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

return (
  <>
    <div>
      <TabMenu>
        {/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
        {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 
                나머지 2개의 tab은 'submenu' 가 됩니다.*/}
        {/* el : 배열에서 처리중인 현재 요소
        index : 배열에서 처리중인 현재 요소의 인덱스 */}
        {menuArr.map((el, index) => {
          return <li key = {index} 
          // 원하는 컴포넌트에 스타일링을 줄 수 있도록 조건부 렌더링을 사용
          className={`${currentTab === index ? "submenu focused" : "submenu"}`} 
          // 리액트 이벤트 핸들러 함수에서는 콜백함수로 전달을 줘야함
          onClick={() => selectMenuHandler(index)}
        >{el.name}</li>})}
        {/* 하드코딩
        <li className="submenu" onClick={() => selectMenuHandler(index)}>{menuArr[0].name}</li>
        <li className="submenu" onClick={() => selectMenuHandler(index)}>{menuArr[1].name}</li>
        <li className="submenu" onClick={() => selectMenuHandler(index)}>{menuArr[2].name}</li> */}
      </TabMenu>
      <Desc>
        {/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
        <p>{menuArr[currentTab].content}</p>
      </Desc>
    </div>
  </>
);
};

Tab.js

📍 새로 알게된 내용

  • Array.includes() : 배열에 특정 값의 유/무 확인 및 특정 값을 반환할때 사용
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-title {
      color: #fff;
    }
    > .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 : 태그를 삭제하는 메소드를 완성하세요.

  // 삭제 클릭한 요소 외에 나머지만 배열에 남아있게 하기 위해 filter메서드를 사용하여 걸러주기
  // index를 사용안하고 el만 인자로 주고 el과 비교하면 모든 요소들이 다 지워짐
  setTags(tags.filter((el, index) => index !== indexToRemove));
};

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

  // includes() : 배열이 항목 중 특정 값을 반환하거나 포함하는지 결정
  if(!tags.includes(event.target.value) && 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)}>
              {/* TODO :  tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
                          삭제 아이콘을 click 했을 때 removeTags 메소드가 실행되어야 합니다. */}
            &times;</span>
          </li>
        ))}
      </ul>
      <input
        className="tag-input"
        type="text"
        onKeyUp={(event) => {
          /* 키보드의 Enter 키에 의해 addTags 메소드가 실행되어야 합니다. */
          {if(event.key === "Enter") {
            addTags(event)
          }
          }
        }}
        placeholder="Press enter to add tags"
      />
    </TagsInput>
  </>
);
};
	

0개의 댓글