본문 바로가기
💻/[과제]

[과제] React Custom Component

by Mia_ 2022. 12. 22.

File Structure

/
├── /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

 

Bare minimum Requirement

 

- React, Styled Components, Storybook을 활용하여 React-custom-component를 완성하기!

- Modal, Toggle, Tab, Tag 컴포넌트 구현하기!


Modal Component 완성하기

 

· Modal UI : 기존의 브라우저 페이지 위에 새로운 윈도우 창이 아닌, 레이어를 까는 것

· TO DO

- isOpen state는 모달 창의 열고 닫힘 여부를 확인할 수 있음 (필요에 따라 state를 더 만들 수도 있음)

- ModalBtn 컴포넌트는 모달 창 제어를 위해 핸들러 함수 openModalHandler를 작성

 → openModalHandler 함수는 ModalBtn 클릭 시 발생되는 change 이벤트 핸들러임 (클릭할 때마다 상태가 불린 값으로 변경)

- ModalView 컴포넌트를 작성하고 isOpen 상태가 true일 경우에만 렌더링

- ModalBackdrop 컴포넌트를 작성하고 isOpen 상태가 true일 경우에만 렌더링

· 요구사항

- ModalContainer : Modal을 구현하는데 필요한 컴포넌트를 감싸주는 컨테이너 컴포넌트 역할을 함 

- ModalBackdrop : Modal이 떴을 때 배경을 깔아주는 역할

- ModalBtn : Modal 창을 키고 끌 수 있는 버튼

- ModalView : Modal 창 컴포넌트

 

Modal.js
1st Try!

- 조건부 렌더링을 통해서 isOpen이 true인 상태에서는 모달 버튼 내부가 'Open!'로, isOpen이 false인 상태에서는 내부가 'Open Modal'이 되도록 구현되어야 한다는 것을 빼먹고 구현함!

- value 값을 활용해서 구현해 본 버전!

..(생략)..

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

  const openModalHandler = (e) => {
    //console.log(e.target.value)
    // TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
    if(e.target.value === "open"){
      setIsOpen(true);
    } else if (e.target.value === "close"){
      setIsOpen(false);
    }
  };

  return (
    <>
      <ModalContainer>
        <ModalBtn
        // TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
        value={'open'} onClick={openModalHandler}
        >
          Open Modal
        </ModalBtn>
        {isOpen ?   
          <ModalBackdrop>
            <ModalView>
              <CloseBtn value={'close'} onClick={openModalHandler}>X</CloseBtn>
              <h4>HELLO CODESTATES!</h4>
            </ModalView>
          </ModalBackdrop>
           : null} 
      </ModalContainer>
    </>
  );
};

 

Modal.js
2nd Try!

- 모달 창 밖을 클릭하면 모달창이 닫히도록 해야함 

- 그래서 state를 바꿀 때 isOpen이 false이면 계속 null값이고 모달 버튼을 눌러 상태가 바뀌면 모달 창이 나타나도록 함 

- 다른 이벤트를 준 곳은 눌으면 창이 사라지게 됨

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 : 300px;
`;

export const ModalBackdrop = styled.div`
  // TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
  position: fixed;
  background-color: rgba(0,0,0,0.15);
  top : 0;
  left : 0;
  right : 0;
  bottom : 0;
  display: flex;
  justify-content : center;
  align-items : center;  
`;

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 CloseBtn = styled.button`
  background-color: white;
  border: none;
  margin-top: 20px;
`;

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

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

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

  return (
    <>
      <ModalContainer onClick={openModalHandler}>
        <ModalBtn
        onClick={openModalHandler}
        >
         {isOpen ? 'Opened!' : 'OpenModal'}
        </ModalBtn>
        {isOpen === false ? null 
        : <ModalBackdrop>
            <ModalView>
              <CloseBtn onClick={openModalHandler}>X</CloseBtn>
              <h4>HELLO CODESTATES!</h4>
            </ModalView>
          </ModalBackdrop>
        }
      </ModalContainer>
    </>
  );
};

 

Modal.js
해설해 주신 코드

추가적으로 학습하게 된 내용

- 불리언 타입의 상태를 바꿀 때의 국룰 

  → 현재 상태 false라면 이번 과제를 예로 들었을 때 setIsOpen(!false) === setIsOpen(true)

  → 현재 상태 true라면 setIsOpen(!true) === setIsOpen(false)

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

  const openModalHandler = (e) => {
    setIsOpen(!isOpen)

    //? 국룰
    // 현재 상태 false
    //setIsOpen(!false) === setIsOpen(true)

    // 현재 상태 ture
    //setIsOpen(!true) === setIsOpen(false)
  };

  return (
    <>
      <ModalContainer onClick={openModalHandler}>
        <ModalBtn
        onClick={openModalHandler}
        >
         {isOpen ? 'Opened!' : 'OpenModal'}
        </ModalBtn>
        : <ModalBackdrop>
            <ModalView onClick={(event) => event.stopPropagation()}>
              <CloseBtn onClick={openModalHandler}>X</CloseBtn>
              <h4>HELLO CODESTATES!</h4>
            </ModalView>
          </ModalBackdrop>
        }
      </ModalContainer>
    </>
  );
};

// ? 이벤트 버블링
// 거품이 올라가는 것처럼 자식 컴포넌트르 눌렀을 때 부모 이벤트가 발생한다
// 눌렀을 때 이벤트가 발생되면 안되는 곳에가서 프로퍼게이션을 주면 됨
// <ModalView onClick={(event) => event.stopPropagation()}>

Toggle Component 완성하기

 

· Toggle UI :  두 가지 상태만 가지고 있는 스위치. 

· TO DO

- isOn state는 토글 버튼의 on/off 여부를 확인할 수 있음 (필요에 따라 state를 더 만들 수도 있음)

- ToggleContainer 컴포넌트는 토글 버튼 제어를 위해 핸들러 함수 togglelHandler를 작성

 → toggleHandler 함수는 ToggleContainer 클릭 시 발생되는 change 이벤트 핸들러임 (클릭할 때마다 상태가 불린 값으로 변경)

 

· 요구사항

- ToggleContainer : Toggle을 구현하는데 필요한 컴포넌트를 감싸주는 컨테이너 컴포넌트 역할을 함 

- Desc : Toggle Switch의 상태를 설명하는 텍스트를 담는 컴포넌트

 

- ToggleContainer 내부에 .toggle-container, .toggle-circle 클래스를 가진 div 요소를 각각 생성

- 생성한 요소에 조건부 스타일링을 활용해 Toggle Switch가 ON인 경우에만 toggle--checked 클래스를 두 요소에 추가

- 조건부 렌더링을 활용해 Toggle Switch가 ON인 상태일 경우 Desc 컴포넌트 내부의 택스트를 'Toggle Switch ON'으로 반대의 경우 OFF 버전으로 변경하기

- 토글 스위치가 부드럽게 옮겨지게 transition 속성 추가 해보기!

 

Toggel.js

- 특정 상태일 때 클래스를 추가해서 스타일링을 주는 것을 학습함

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;
  }
 
  > .toggle--checked {
    background-color: var(--coz-purple-600);
    transition: all 0.5s ease-in-out;
  }

  > .toggle-circle {
    position: absolute;
    top: 1px;
    left: 1px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background-color: #ffffff;
    transition: all 0.5s ease-in-out;
  }

  > .move {
    left : 27px !important;
    transition: all 0.5s ease-in-out;
  }
`;

const Desc = styled.div`
  // TODO : 설명 부분의 CSS를 구현합니다.
  position: relative;
  margin-top: 1rem;
  text-align: center;
`;

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

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

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

 

Toggel.js
해설 해주신 코드

추가적으로 학습한 내용

- 위에서 내가 구현한 방법은 circle div에는 아예 move라는 클래스를 추가적으로 줬는데 &을 이용해 구현

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;
    &.toggle--checked{
      background-color: var(--coz-purple-600);  
    }
  }

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

const Desc = styled.div`
  position: relative;
  margin-top: 1rem;
  text-align: center;
`;

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

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

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

 


Tab Component 완성하기

 

· Tab UI : 동일한 메뉴라인에서 뷰를 전환할 때 사용

· TO DO

- currentTab state는 현재 tab의 index를 확인할 수 있음 (필요에 따라 state를 더 만들 수도 있음)

- TabMenu 컴포넌트는 버튼 제어를 위해 핸들러 함수 selectMenuHandler 를 작성

 → toggleHandler 함수는 TabMenu 클릭 시 발생되는 change 이벤트 핸들러임 (클릭할 때마다 상태가 index 값으로 변경)

- li 요소를 이용해 메뉴를 생성하고, 각 메뉴를 눌렀을 때 뷰가 전환되도록 handler(selectMenuHandler) 함수를 작성하기!

- 조건부 스타일링과 currentTab 상태를 이용하여 클릭한 Tab 메뉴만 className(submenu focused)과 CSS가 변경되도록 구현 --> 템플릿 리터럴과 삼항 연산자 활용하기!

 

· 요구사항

- TabMenu 

 → TabMenu 내부에 .submenu 클래스 명을 가진 li 요소들을 map을 이용한 반복을 통해 생성함

 → TabMenu 내부에 .submenu 클래스 명을 가진 li 요소들의  textContent는 각 요소의 name임

-  currentTab

 → 조건부 렌더링을 활용해서 Tab 메뉴가 선택된 상태일 때, 선택된 Tab 메뉴 li 요소의 클래스명만 submenu focused가 되어야 하고, 선택되지 않은 나머지는 submenu가 되도록 구현하기

 → TabMenu를 클릭하면 현재 선택된 탭의 인덱스 값을 전달받아 currentTab 상태를 변경하는 selectMenuHandler 메서드가 실행되어야 함

 → TabMenu를 클릭하면 현재 선택된 탭 메뉴만 .focused CSS가 적용되어야 함 

 → TabMenu를 클릭하면 Desc 컴포넌트의 content의 내용이 해당 탭의 content로 바뀌어야 함

 

Tab.js
1st Try!

- 주어진 메뉴 배열에 id를 임의로 부여해서 구현은 성공함!

- but, 임의로 추가한 id로 테스트는 성공하지 못함..

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

  const menuArr = [
    { id:0, name: 'Tab1', content: 'Tab menu ONE' },
    { id:1, name: 'Tab2', content: 'Tab menu TWO' },
    { id:2, name: 'Tab3', content: 'Tab menu THREE' },
  ];

  const selectMenuHandler = (li) => {
    setCurrentTab(li.target.value);
  };

  return (
    <>
      <div>
        <TabMenu>
          { menuArr.map((li) => {
            return (
              <li value={li.id} className="submenu" onClick={selectMenuHandler}>{li.name}</li> 
            );
          })}
        </TabMenu>
        <Desc>
          { menuArr.filter(li => currentTab === li.id).map((li) => {
            return (
              <p>{li.content}</p> 
            );
          })}
        </Desc>
      </div>
    </>
  );
};

 

Tab.js
2nd Try!

- TabMenu에 map을 이용해서 뿌려줄 때 menuArr 배열에 요소를 뿌리면서 key값을 줌 → 뿌리면서 index라는 이름으로 key 값 생성

- 그리고 li를 클릭하면 selectMenuHandler에 index를 인자로 전달하면서 실행  실행되면서 상태가 변경되고 재랜더링

import { useState } 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;

  .submenu {
    height: 50px;
    width: 30%;
    display: flex;
    align-items: center;
    padding: 15px;
    transition: all 0.18s ease-in-out;
  }

  .focused {
    background-color: var(--coz-purple-600);
    color: #ffff;
  }

  & 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 가 갱신되도록 함수를 완성하세요.
    //console.log(index);
    setCurrentTab(index);
  };

  return (
    <>
      <div>
        <TabMenu>
          {/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다. - done!*/}
          {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 나머지 2개의 tab은 'submenu' 가 됩니다.*/}
          { menuArr.map((tab,index) => {
            return (
              <li key={index} className={`${currentTab === index ? 'submenu focused' : 'submenu'}`} onClick={() => selectMenuHandler(index)}>{tab.name}</li> 
            );
          })}
        </TabMenu>
        <Desc>
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

Tag Component 완성하기

 

· Tag UI : 레이블 지정을 통해 구성이나 분류에 도움이 되는 키워드 집합을 만들 때 자주 사용 됨.

· TO DO

- tags state는 배열의 형태, 초기값으로 initalTages를 가지고 있음 (필요에 따라 state를 더 만들 수도 있음)

- TagsInput 컴포넌트는 핸들러 함수 addTags를 가짐

 → addTags 함수는 input창에 Enter키를 누를 때만다 발생하는 change 이벤트 핸들러 (Enter 입력할 때마다 입력된 값이 state에 추가 됨)

- span.tag-close-icon 에는 아직 실제 작동하는 삭제 아이콘(X)이 없음! → 삭제 아이콘을 만들고, 이 버튼이 동작하도록 handler(removeTags) 함수를 작성하기!

 

· 요구사항

- input 기능 테스트 

 → input 창에 텍스트를 입력 후 Enter 키를 누르면 태그가 추가되어야 함! 

 → Enter 키를 통해 태그가 추가되도록 하며, Enter 키가 눌리면 태그르 추가하는 addTags 메서드가 실행되어야 함!

 → addTags 메서드는 태그 추가 기능 이외에 아래 세가지 기능도 수행할 수 있어야 함

    1. 이미 입력되어 있는 태그인지 검사하야 이미 입력되어 있다면 추가히지 말아야 함

    2. 아무것도 입력하지 않은 상태에서는 Enter 키를 눌러도 addTags 메서드가 실행되지 않아야 함

    3. 태그가 추가되고 나면 input 창이 비워져야 함

 

- 삭제 기능 테스트

 → 기본적으로 tags 배열 안에 모든 태그들이 화면에 보여야 함 

 → 태그 이름 옆에 삭제 아이콘이 표시되도록 하고, 아이콘을 클릭하면 해당 태그를 삭제하는 removeTag 메서드가 실행되어야 함

 → removeTags 메서드가 삭제 아이콘이 눌린 태그를 삭제하도록 removeTags 메서드를 완성해야 함

 

 

Tag.js

- 구현은 다 했는데 몇 개의 테스트를 통과하지 못했음 

- 삭제 버튼을 구현할 때 삭제를 의미하는 X를 p태그로 감싸고 p태그에 onClick 이벤트를 줬더니 통과하지 못했음 → span 태그 자체에 이벤트를 옮겨줘서 통과

- input에 addTags 메서드를 연결할  때, 키를 눌렀다가 땔 때의 이벤트를 받아와서 전달해주는 과정에서 고전함

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: 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) => {
    //console.log('Click!') //-->  확인 됨!
    //console.log(indexToRemove); //--> key 값이 뜸

    console.log(tags); 
    tags.splice(indexToRemove,1)
    setTags([...tags])
    
  };

  const addTags = (event) => {
    const newTag = event.target.value;
    // TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성.
    // - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
    // - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
    // - 태그가 추가되면 input 창 비우기
    //console.log(event.key) //--> Enter  키 누르면 나오는 거 확인함
    //console.log(event.target.value) //--> 확인 됨
    
    if(event.key === 'Enter' && !tags.includes(newTag) && newTag){
      setTags([...tags, newTag]);
      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={(index) => removeTags(index)}>
                <p>X</p> 
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          onKeyUp={(event) => {addTags(event)}}
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

'💻 > [과제]' 카테고리의 다른 글

[과제] React Hooks 적용하기  (0) 2023.01.26
[과제] C Market 상태 관리하기  (0) 2022.12.29
[과제] JSON.stringify  (0) 2022.12.16
[과제] Tree UI  (0) 2022.12.16
[과제] 나만의 아고라 스테이츠 서버 만들기  (0) 2022.12.11