React Custom Component

ock·2023년 2월 22일
0

오늘은 어제에 이어 과제 마무리한 날.




1. Modal


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%;
`;

export const ModalBackdrop = styled.div`
  // TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
  background-color: rgba(0,0,0,0.5);
  position: fixed;
  left: 0;
  right: 0;
  top: 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: 3k0px;
  cursor: grab;
`;

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


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

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

  return (
    <>
      <ModalContainer>
        <ModalBtn onClick = {openModalHandler}
        // TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 
		//변경하는 메소드가 실행되어야 합니다.
        >
          {isOpen ? "Opend!" : "Open Modal"}
          {/* TODO : 조건부 렌더링을 활용해서 Modal이 
          열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가
          'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 
          ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}

        </ModalBtn>
        {/*isOpen이 true이면 모달창을 열고, false이면 null값을 준다.*/}
        {isOpen ? 
        <ModalBackdrop onClick={openModalHandler}>
          <ModalView onClick = {event=>event.stopPropagation()}>
            {/*event.stopPropagation() = 이벤트가 상위 엘리먼트에게 
            전달되지 않게 막아준다.
            => 이것 때문에 모달창이 떴을 때 배경을 클릭하면 모달창이 꺼지지만,
            모달창 내부를 클릭하면 아무일도 일어나지 않는다.*/}
            <button onClick={openModalHandler}>&times;</button>
            <div>HELLO CODESTATES!</div>
          </ModalView>
        </ModalBackdrop>: null}
        {/* TODO : 조건부 렌더링을 활용해서 Modal이 
        열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 
        구현해야 합니다. */}
      </ModalContainer>
    </>
  );
};

//자식 요소에서 이벤트가 발생했을 때,
//부모요소에서도 같은 이벤트가 발생한 것처럼 동작
//=> 이벤트 버블링
//막기위해서는? event => event.stopPropagation()

이 코드에서는 useState hook을 사용하여 isOpen이라는 상태값을 관리하고 있다. isOpen의 값이 true이면 모달이 열리고, false이면 모달이 닫힌다.

openModalHandler 함수는 ModalBtn이 클릭될 때 실행되는 함수이다. 이 함수에서는 setIsOpen 메소드를 호출하여 isOpen의 상태값을 반대로 변경한다.

ModalBtn은 모달을 열고 닫는 역할을 한다. 버튼의 내부 텍스트는 isOpen의 값에 따라 'Open Modal' 또는 'Opened!'로 바뀌도록 구현되어 있다.

ModalBackdrop은 모달이 열렸을 때, 배경이다. 배경을 클릭하면 모달이 닫히도록 구현되어 있다.

ModalView는 실제 모달 내부의 컨텐츠를 담당하는 역할을 한다. ModalView 내부에는 X 버튼과 "HELLO CODESTATES!" 라는 텍스트가 들어가 있다.

마지막으로, 조건부 렌더링을 통해 isOpen의 값에 따라 모달이 렌더링될지 말지를 결정한다. isOpen이 true일 때는 모달, false일 때는 null 값을 반환하여 모달이 닫히게 된다.




2.Toggle


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;
    transition: 0.5s;

    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    &.toggle--checked{
      background-color: orange;
      transition: 0.5s;
    }
  }

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

    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    &.toggle--checked {
      left: 27px;
      transition: 0.5s;
      }

  }
`;

const Desc = styled.div`
  // TODO : 설명 부분의 CSS를 구현합니다.
  text-align: center;
  padding: 20px;
`;

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

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

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

Toggle 스위치를 구현하기 위해서는 상태(state)를 이용하여 스위치의 On/Off 여부를 기억해야 한다. 이 코드에서는 useState hook을 이용하여 isOn 상태를 관리하고 있다.

toggleHandler() 함수는 스위치를 클릭하면 실행되며, 현재 isOn 상태를 반전시키는 메서드다. 즉, 스위치를 클릭할 때마다 isOn 상태가 toggle되어 true/false가 변경된다.

return문에서는 toggle-container와 toggle-circle 클래스를 가진 div 엘리먼트 두 개를 출력한다. toggle-container는 스위치의 배경을 의미하고, toggle-circle은 스위치의 공이 움직이는 부분을 의미한다. 이 때 isOn이 true(스위치가 켜진 상태)일 때는 두 엘리먼트에 toggle--checked 클래스가 추가되어 조건부 스타일링을 적용한다.

마지막으로 Desc 컴포넌트를 출력하는데, 이는 스위치의 On/Off 상태를 표시하는 역할을 한다. isOn이 true일 때는 'Toggle Switch ON', false일 때는 'Toggle Switch OFF'가 출력된다.




3.Tab


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

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

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 {
    ${'' /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
    padding: 10px;
    width: calc(100% / 3);
  }

  .focused {
    ${'' /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다.  */}
    background-color: pink;
    color: white;
    transition: 0.3s;
  }

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

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

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

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

  return (
    <>
      <div>
        <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>*/}
              {/*map함수의 두번째 인자로 index를 넣어서 핸들러 함수에 전달해주는 것이 핵심 */}
              {menuArr.map((el,index)=>{
                return<li key={index}
                         className={index === currentTab ? 'submenu focused':'submenu'}
                         onClick={()=>selectMenuHandler(index)}>{el.name}</li>
              })}
        </TabMenu>
        <Desc>
          {/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

초기값으로 menuArr 상수에 3개의 Tab 메뉴와 그에 대한 내용이 담긴 객체가 저장되어 있다.

currentTab 상태는 현재 선택된 Tab 메뉴의 인덱스를 저장한다. setCurrentTab 함수를 통해 상태를 갱신한다.

selectMenuHandler 함수는 클릭 이벤트가 발생했을 때, 현재 선택된 메뉴의 인덱스 값을 받아와 상태를 갱신한다.

map 함수를 사용하여 Tab 메뉴를 반복하여 보여준다. 각 Tab 메뉴는 submenu 클래스를 가지고 있으며, 현재 선택된 메뉴는 focused 클래스를 가진다.

Tab 메뉴를 클릭할 때, selectMenuHandler 함수가 실행되어 현재 선택된 Tab 메뉴의 인덱스를 currentTab 상태로 갱신한다.

각 Tab 메뉴의 내용은 menuArr 배열에서 현재 선택된 Tab 메뉴의 인덱스를 사용하여 가져온다. Desc 컴포넌트에 보여지는 내용은 menuArr 배열에서 현재 선택된 Tab 메뉴의 내용이 표시된다.




4.Tag


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: white;
      }
      > .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 : 태그를 삭제하는 메소드를 완성하세요.
    const filter = tags.filter((el,index)=>index !== indexToRemove);
    setTags(filter);
  };

  const addTags = (event) => {
    // TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
    // 이 메소드는 태그 추가 외에도 아래 3 가지 기능을 수행할 수 있어야 합니다.
    // - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
    // - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
    // - 태그가 추가되면 input 창 비우기
    console.log(event)
    if(event.key === "Enter" && 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)
              }>&times;
                {/* TODO :  tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
                            삭제 아이콘을 click 했을 때 removeTags 메소드가 실행되어야 합니다. */}
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          onKeyUp={(e) => addTags(e)}
            /* 키보드의 Enter 키에 의해 addTags 메소드가 실행되어야 합니다. */
          
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

태그 입력창에는 초기값으로 'CodeStates'와 'kimcoding'이 들어가 있다.

태그를 추가하는 기능은 태그 입력창에 키보드로 값을 입력하면서 Enter 키를 누르면 동작한다. 이때, 입력된 값이 비어있지 않고, 이미 추가된 태그 중에 없는 값이면, 해당 값을 추가한다. 추가한 후에는 입력창을 비운다.

태그를 삭제하는 기능은 추가된 태그 옆에 x 기호가 표시되며, 클릭하면 해당 태그가 삭제된다. 이때, 삭제할 태그의 인덱스를 파라미터로 전달하여 removeTags 함수를 실행한다.

태그를 추가 및 삭제하는 과정에서는 useState hook을 사용하여 tags라는 배열에 상태를 저장하고, 해당 상태를 수정하는 setTags 함수를 사용한다.

태그를 추가 및 삭제하는 과정에서는 아래와 같은 3가지 기능을 수행한다.

  • 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
  • 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
  • 태그가 추가되면 input 창 비우기
profile
어제의 ock보다 성장한 오늘의 ock_FE 개발자

0개의 댓글