[react] 토글/태그/모달/탭 구현하기

Updated:

Categories:

Tags: , ,

📌 개인적인 공간으로 공부를 기록하고 복습하기 위해 사용하는 블로그입니다.
정확하지 않은 정보가 있을 수 있으니 참고바랍니다 :😸
[틀린 내용은 댓글로 남겨주시면 복받으실거에요]

모달

모달을 구현하는 법 보다 CSS가 더 어려웠다.😥

ModalContainer

처음에 중앙으로 구현을 못해서 제일 상위에 해당하는 컨테이너에서 버튼을 중앙으로 배치하기 위해 조정했다.

1
2
3
4
5
6
export const ModalContainer = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
`

ModalBackdrop - modal 이 열렸을 때 뒷 배경 부분

1
2
3
4
5
6
7
8
export const ModalBackdrop = styled.div`

  position: fixed;
  width: 1280px;
  height:450px;
  opacity: 0.8;
  background-color: gainsboro;
`;

position을 잘 몰라서 헤맸는데

  • 기본값은  static이 적용된다 : HTML 문서 상에서 원래 있어야 하는 위치에 배치
  • relative로 설정하게 되면, 요소를 원래 위치에서 벗어나게 배치할 수 있는데  top, bottom, left, right 속성을 이용해서, 요소가 원래 위치에 있을 때의 상하좌우로부터 얼마나 떨어지게 할지를 지정 가능하다.

  • fixed 는 스크롤을 위아래로 움직여도 화면에 고정되어서 움직이지 않는 것.
  • absolute는 가장 가까운 상위 요소를 기준으로 top, left, bottom, right 속성을 적용한다.
  • sticky 는 모달 또는 그 부분이 포함된 요소 내에서 고정된다,
  • [position: sticky] 적용 하면 아래와 같이 버튼과 컨테이너가 분리된다…

ModalButton

1
2
3
4
5
6
7
8
9
export const ModalBtn = styled.button`
  background-color: pink;
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
  cursor: grab;
`;
  • 버튼은 기본적으로 구성했고 버튼에 커서를 가까이 대면 커서가 바뀌도록 grab으로 지정했다. 실제로 가까이 가면 손 모양이 나온다.

  • cursor: grab;

    : 요소를 잡을 수 있다는 손 모양이 나온다(즉, 끌 수 있다는 것).

  • cursor: grabbing;

    : 해당 요소가 현재 잡히고 있음을 나타낸다(즉, 끌고 있음을 나타냄).

  1. State & 이벤트

    1
    2
    3
    4
    5
    6
    
     export const Modal = () => {
       const [isOpen, setIsOpen] = useState(false);
        
       const openModalHandler = (e) => {
         setIsOpen(!isOpen);
       };
    
    • const [isOpen, setIsOpen] = useState(false); 로 상태 초기화, setIsOpen함수는 상태를 업데이트하는 데 사용
    • 이벤트 처리 : openModalHandler ⇒ 버튼 클릭시 modal이 닫히면 열리고 열려있으면 닫히게 설정.
    • 버튼을 눌러서 모달이 열렸을 때 Open, 닫혔을때는 Open Modal로 버튼 위 텍스트를 설정
  2. 모달 버튼

    1
    2
    3
    4
    
     <ModalContainer>
             <ModalBtn onClick={openModalHandler}>
               {isOpen ? 'Opened!' : 'Open Modal'}
             </ModalBtn>
    
  3. 열려 있을 때(isOpen, true) 모달 배경과 뷰가 보이도록 함

    1
    2
    3
    4
    5
    6
    7
    8
    
      {isOpen && (
               <ModalBackdrop onClick={openModalHandler}>
                 <ModalView onClick={(e) => e.stopPropagation()}>
                   <h2>😛메롱👅</h2>
                   <button onClick={openModalHandler}>Close</button>
                 </ModalView>
               </ModalBackdrop>
             )}
    
    • ModalBackdrop트리거를 클릭하면 openModalHandler모달이 닫힌다.
    • ModalView는 내부를 클릭해도 모달이 닫히지 않도록 하려면 클릭 이벤트가 배경으로(ModalBackdrop) 전파되는 것을 막아야 한다.

      1
      
        <ModalView onClick={(e) => e.stopPropagation()}>
      

      이렇게 하면 ModalBackdrop모달 뷰 내부를 클릭할 때 모달이 열린 상태로 유지된다

완성작

토글

ToggleContainer CSS

  1. ToggleContainer

    1
    2
    3
    4
    5
    
     const ToggleContainer = styled.div`
       position: relative;
       margin-top: 8rem;
       left: 48%;
       cursor: pointer;
    
    • 화면 중앙에 배치하기 위해서 margin-top: 8rem; left: 48%; 을 설정
  2. .toggle-container , 토글 스위치 배경

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
       > .toggle-container {
         width: 50px;
         height: 24px
         border-radius: 30px;
         background-color: #8b8b8b;
            
          
         &.toggle--checked{
           background-color: pink;
         }
       }
        
    
    • toggle—checked 될 경우 (토글이 겨질 경우) 배경색이 변하도록 지정.
  3. .toggle-circle, 토글 스위치 내 버튼(원) 스타일

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
       > .toggle-circle {
         position: absolute;
         top: 1px;
         left: 1px;
         width: 22px;
         height: 22px;
         border-radius: 50%;
         background-color: #ffffff;
         // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
        
         &.toggle--checked {
           transform: translateX(26px);
         }
       }
     `;
        
     const Desc = styled.div`
       font-size: 1em;
     `;
    
    • 컨테이너 내부에 원을 넣기 위해서 position: absolute; top: 1px; left: 1px; 속성 지정
    • 토글이 켜질 경우 원이 오른쪽으로 이동하게 함. 26px 만큼!
    • transform 속성 - translate(이동) / scale(사이즈 조절) / rotate(회전) / skew (왜곡)
    • Dsec : toggle 아래에 description 부분

Toggle Component

1
2
3
4
5
6
export const Toggle = () => {
  const [isOn, setisOn] = useState(false);

  const toggleHandler = () => {
    setisOn(!isOn);
  };
  • 상태는 false로 초기화되어 있고 클릭시 바뀐다 → ON( true) 또는 OFF( false)상태로 변경된다.

ToggleSwitch 렌더링

1
2
3
4
5
6
7
8
9
10
11
  return (
    <>
      <ToggleContainer onClick={toggleHandler}>
      <div className={`toggle-container ${isOn ? "toggle--checked" : ""}`} />
      <div className={`toggle-circle ${isOn ? "toggle--checked" : ""}`} />
      </ToggleContainer>
      <br/>
      <Desc><center>{isOn ? 'Toggle Switch ON' : 'Toggle Switch OFF'}</center></Desc>
  
    </>
  );
  • <ToggleContainer onClick={toggleHandler}> : click 이 눌려지면 toggleHandler 를호출한다
  • toggle-container와 toggle-circle은 isOn이 true라면 CSS에서 갖고 있는 toggle—checked의 지정된 속성으로 불러온다.
  • toggle description 분은 isOn이 true/false일 경우 보이는 문장이 다르게 설정

완성작

TabMenu - CSS

  1. TabMenu - 전체 탭의 속성 지정.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     const TabMenu = styled.ul`
       background-color: lightsalmon;
       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;
    
    • 컨테이너 내에서 탭을 가운데 정렬 하기 위해 justify-content: center; align-items: center; 스타일 지정
    • display: flex 사용하여 수평으로 정렬
  2. submenu : 개별 탭 스타일 지정

    1
    2
    3
    4
    
       .submenu {
         cursor: pointer;
         transition: 0.3s ease;
       }
    
    • 클릭하면 색상 전환시 0.3초동안 부드럽게 전환 하도록 trainsition : 0.3s ease 속성 넣었고 cusor는 탭 메뉴에 마우스 커서를 올리면 손가락이 가르키는 모양으로 변하게 설정
    • ease 속성은 CSS에서 애니메이션의 시작과 끝을 천천히, 중간을 빠르게 진행하도록 하는 기본 가속 곡선

Dsec - CSS

1
2
3
const Desc = styled.div`
  text-align: center;
`;
  • Dsec(=Description)는 탭 콘텐츠를 표시하는 컨텐츠 영역의 스타일.
  • 별도 지정없이 가운데 정렬만 할 수 있게 구현

Tab Component

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
  };
  • 상태 초기화 : const [activeIndex, setActiveIndex] = useState(0) ⇒ 선택한 tab의 index를 설정하고 set으로 업데이트 한다.
  • 이벤트 처리 : selectMenuHandler 함수는 탭을 클릭하면 호출되어 클릭한 탭의 인덱스를 전달하고 클릭한 탭의 인덱스로 상태를 업데이트한다.

Return문 , 화면렌더링

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
return (
    <>
      <div>
        <TabMenu>
          {menuArr.map((el,index)=>(
            <li
            key ={index}
              className={`submenu ${currentTab===index? 
              'focused' : '' }`}
              onClick={() => selectMenuHandler(index)}
            >
            {el.name}
            </li>
          ))}
        </TabMenu>
        <Desc>
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
  • map을 사용해서 순회하여 각 요소에 대해 li 요소를 생성
  • key={index}: 각 li 요소에 고유한 키를 부여
  • className={submenu ${currentTab === index ? ‘focused’ : ‘’}} 현재 탭의 인덱스를 찾고 인덱스가 같으면 'submenu focused' 클래스를 적용하고, 그렇지 않으면 'submenu' 클래스를 적용
  • onClick={() => selectMenuHandler(index)}: 탭을 클릭하면 selectMenuHandler 함수가 호출되어 currentTab을 해당 index로 업데이트한다.
  • {menuArr[currentTab].content} : 현재 선택된 tab의 content를 가져와서 보여주는 js 문법이고
  • 는 위에 서 정의한 Desc라는 styled-component를 사용하여 콘텐츠를 표시하기 위한 태그이다.

참고

나중에 프론트엔드 실무에 들어가면 idx를 사용하면 최적화가 안될 수 있다 ⇒ 삭제가 빈번할 때는 배열의 요소를 자르는 것이 효율이 좋지 않다. 외부 라이브러리나 다른 프롬프트를 사용하는 것이 좋음

태그

CSS

TagsInput

1
2
3
4
5
6
7
8
9
10
11
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;

  • min-height: 48px; width: 480px;: 컨테이너의 최소 높이와 너비를 설정
  • flex-wrap 컨테이너가 더이상 아이템을 한줄에 담을 여유공간이 없을 때 줄바꿈 속성을 정하는 것.
    • wrap 은 줄바꿈
    • nowrap 삐져나감
    • wrap-reverse 위로 올라감.

>ul

1
2
3
4
5
 > ul {
    display: flex;
    flex-wrap: wrap;
    padding: 0;
    margin: 8px 0 0 0;
  • > : 바로 하위에 있는 요소를 선택하는 것 (자식선택자라고 주로 얘기한다…) 즉 TagsInput의 바로 하위에 있는 자식인 ul을 선택하는 것.

>.tag

1
2
3
4
5
6
7
8
9
10
11
12
13
  > .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);
  • TagsInput 아래의 ul의 바로 아래 에 있는 tag의 스타일을 적용

>.tag-close-icon

1
2
3
4
5
6
7
8
9
10
11
12
13
   > .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;
      }
  • TagsInput 아래의 ul의 바로 아래 에 있는 tag-close-icon의 스타일을 적용

>input

1
2
3
4
5
6
7
8
9
10
11
12
13
  > 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);
  }
  • TagsInput의 바로 하위에 있는 자식인 input의 CSS를 적용
  • :focus { outline: transparent; }: 포커스가 있는 기본 윤곽선을 제거
  • &:focus-within : & 은 현재 선택자를 참조하여 중첩되는 스타일을 적용하는 것. 입력을 하고 있으면 테두리 색상이 변경된다.

Tag Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export const Tag = () => {
  const initialTags = ['Code', 'kimcoding'];
  const [tags, setTags] = useState(initialTags);

  const removeTags = (indexToRemove) => {
    setTags(tags.filter((_, index) => index !== indexToRemove));
  };

  const addTags = (event) => {
    const tag = event.target.value.trim();
    if (event.key === 'Enter' && tag) {
      if (!tags.includes(tag)) {
        setTags([...tags, tag]);
        event.target.value = '';
      }
    }
  };

  1. 초기 값은 initialTags로 구현
  2. removeTags : tag 제거기능 - 선택한 요소의 idx를 찾아서 제거.
    • 제거할 인덱스(indexToRemove)를 받고
    • filter 메서드에서 첫번째 매개변수는 element 인데 사용히지 않기 때문에 언더바로 표시, 두번째 매개변수는 현재 요소의 인덱스이다.
    • index !== indexToRemove 조건을 검사합니다. 이 조건이 true일 경우 해당 요소는 새로운 배열에 포함되고, false일 경우 포함되지 않는다.
  3. addTags는 enter를 눌렀을 때 새로운 태그 추가하는데 입력 값을 잘라내고 빈 문자열이 아닌지 확인 후 추가한다. 또한 중복된 값이 있는지 검증도 한다.
  4. 태그가 유효하고 고유하면 태그가 tags상태에 추가되고 입력 필드가 지워진다 - 화면 렌더링 됨
  5. event.target.value = ‘ ’ ; tag가 만들어지면 inptu창이 비워져야 하므로 공백으로 변경된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  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>
    </>
  );
};
    • 순서없는 목록으로 각 요소를 구성 → 각 요소는
    • 에 해당
  1. map으로 배열을 순회하고 각 태그에 대한 <li>를 반환한다.
    • key={index}: keyprop은 React에서 어떤 항목이 변경되었는지, 추가되었는지, 제거되었는지 식별하는 데 사용
    • {tag} : tag의 이름
    • <span className=”tag-close-icon” onClick={() => removeTags(index)}>×</span> : tag의 닫기 아이콘, 클릭하면 removetTags라는 함수를 호출하고 인덱스를 인자로 전달한다.
  2. <input className=”tag-input” …/> : 새로운 태그를 입력하는 필드
    • onKeyUp={(event) => addTags(event)} : 입력이 포커스 되어 있는 동안 Enter 키를 누렀다가 놓을 때 만 함수를 호출한다.
  3. keyup 과 keydown
    • keydown 이벤트는 키가 눌렸을 때 발생하고, 키를 계속 누르고 있는 동안 반복적으로 발생
    • keyup 이벤트는 키를 눌렀다가 손을 뗐을 때 발생하며, 한 번만 트리거 되기때문에 중복 이벤트와 auto-repeat 현상이 없고, 보다 예측 가능하고 안정적인 동작을 제공

완성작

flex 어려워서 참고한 사이트 : https://studiomeal.com/archives/197

react 카테고리 내 다른 글 보러가기

Leave a comment