본문 바로가기
프로그래밍/웹 개발

[React] Hook과 Styled Component를 통한 기능 구현

by 제이콥J 2021. 8. 21.

코드스테이츠의 과제로 React의 Hook과  이벤트 핸들러를 통해 Modal, Toggle, Tab, Tag 기능을 구현해보았다.

동시에 Styled Component를 활용하여 디자인을 했다.

아래과 같은 방식으로 진행했으며, 컴포넌트를 구성하는 코드를 정리해보았다.

 

1. 기능별로 컴포넌트 구성 : Modal, Toggle, Tab, Tag, Autocomplete, ClickToEdit 컴포넌트 구성

2. 컴포넌트들을 App.js에 import하여 연결하기

 

Modal 컴포넌트 제작

왼쪽 : 클릭 전   /   오른쪽 : 클릭 후

1. Styled Component 기능으로 div와 button에 대해 CSS로 디자인 

2. useState Hook을 사용하여 Open 여부에 대한 상태 관리 (True or False)

3. 이벤트 핸들러를 통해 버튼을 클릭하면 State가 true-false로 바뀌도록 구현

4. 상태가 true/false 여부에 따라 각각 다른 Styled Component를 화면에 표시

 

코드 작성

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

// div 태그에 Modal을 구현하는데 전체적으로 필요한 CSS를 구현
export const ModalContainer = styled.div`
  // Open Modal 글자의 위치를 중간으로 이동
  height: 15rem;
  text-align: center;
  margin: 120px auto;
`;

// div 태그에 Modal이 떴을 때의(클릭 후) 배경을 깔아주는 CSS를 구현
export const ModalBackdrop = styled.div`
  background-color: greenyellow;
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
`;

// button 태그에 Modal 버튼을 보여주는 CSS를 구현
export const ModalBtn = styled.button`
  background-color: #4000c7;
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
  cursor: grab;
`;

// div 태그로서 Modal이 떴을 때 Modal 창의 CSS를 구현
  // attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가
export const ModalView = styled.div.attrs(props => ({ 
  role: 'dialog'
}))`
  // Modal창 CSS를 구현
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
`;


// Modal 컴포넌트 규현
export const Modal = () => {

  // Modal 오픈 여부를 State로 관리
  const [isOpen, setIsOpen] = useState(false);

  // 이벤트 핸들러 함수로 state를 변경
  const openModalHandler = (e) => {
    setIsOpen(!isOpen)
  };

  return (
    <>
      <ModalContainer>
        <ModalBtn onClick={openModalHandler}> 
          {isOpen ? "Opened!" : "Open Modal"}
        </ModalBtn>
        {isOpen ? (<ModalBackdrop />)&&(<ModalView />) : ""}
      </ModalContainer>
    </>
  );
};

 

레퍼런스 코드 일부

레퍼런스 코드로 구현

 

export const ModalBackdrop = styled.div`
  position: fixed;
  z-index: 999;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background-color: rgba(0,0,0,0.4);
  display: grid;
  place-items: center;
`;

export const ModalView = styled.div.attrs(props => ({
  role: 'dialog'
}))`
    border-radius: 10px;
    background-color: #ffffff;
    width: 300px;
    height: 100px;
    > div.close_btn {
      margin-top: 5px;
      cursor: pointer;
    }
    > div.desc {
      margin-top: 25px;
      color: #4000c7;
    }
`;

return (
    <>
      <ModalContainer>
        <ModalBtn onClick={openModalHandler}>
          {isOpen === false ? 'Open Modal' : 'Opened!'}
        </ModalBtn>
        {isOpen === true ? <ModalBackdrop onClick={openModalHandler}>
          <ModalView onClick={(e) => e.stopPropagation()}>
            <span onClick={openModalHandler} className='close-btn'>&times;</span>
            <div className='desc'>HELLO CODESTATES!</div>
          </ModalView>
        </ModalBackdrop> : null}
      </ModalContainer>
    </>
  );

 

Toggle 컴포넌트 제작

Toggle 구현

1. Styled Component 기능으로 div의 Class별로 CSS 디자인하기

2. useState Hook을 사용하여 토글의 On/Off 여부에 대한 상태 관리 (True or False)

3. 이벤트 핸들러를 통해 버튼을 클릭하면 State가 true-false로 바뀌도록 구현

4. 상태가 true/false 여부에 따라 각각 다른 Styled Component의 Class를 화면에 표시

 

코드 작성

.toggle--checked 클래스의 코드는 각각 .toggle-container, .toggle-circle 클래스 아래에 작성되어야 함

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

// 클래스를 설정하며 .toggle--checked 클래스가 활성화 되었을 경우의 CSS도 구현
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: all .2s ease;
    
  }
  
  >.toggle--checked {
      background-color: #ac88c2;
    }

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

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

// State를 바꿔줄 이벤트 함수 구현
export const Toggle = () => {
  const [isOn, setisOn] = useState(false);

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

  return (
    <>
      <ToggleContainer onClick={toggleHandler}>
        {/* 조건부 스타일링 : Toggle이 ON일 경우 toggle--checked 클래스를 추가 */}
        <div className = {`toggle-container ${isOn ? "toggle--checked" : ""}`}/>
        <div className = {`toggle-circle ${isOn ? "toggle--checked" : ""}`}/>
      </ToggleContainer>
      
      {/* 조건부 렌더링 : Toggle의 ON/OFF 상태에 따라 Desc 컴포넌트 내부의 텍스트 변경 */}
      <Desc> {isOn ? "Toggle Switch ON" : "Toggle Switch OFF"} </Desc>
    </>
  );
};

 

레퍼런스 코드

.toggle--checked 클래스의 코드를 각각 .toggle-container, .toggle-circle와 연결해줌

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: all .2s ease;
        &.toggle--checked {
            background-color: #4000c7;
        }
    }
    > .toggle-circle {
        position: absolute;
        top: 1px;
        left: 1px;
        width: 22px;
        height: 22px;
        border-radius: 50%;
        background-color: #fafafa;
        transition: all .25s ease;
        &.toggle--checked {
            left: 27px;
        }
    }
`;

 

Tab 컴포넌트 제작

Tab 구현

1. Styled Component 기능으로 div 엘리먼트의 Class별로 CSS 디자인하기

2. 화면에 표시되는 내용을 배열의 엘리먼트에 넣고, 추후 인덱스로 이 값을 불러 올 예정

3. useState를 통해 인덱스를 상태로 관리하고, 상태로서의 인덱스가 바뀔 때마다 CSS 디자인과 화면 문구가 달라짐

4. 이벤트 핸들러로 상태 값인 인덱스를 변경해주며, 이때 이벤트 객체가 아닌 map 함수의 index 전달인자를 사용

 

코드 작성

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

// Styled-Component로 TabMenu 컴포넌트의 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;

  .submenu {
    width: 100%;
    padding: 15px 10px;
    cursor: pointer;
  }

  .focused {
    background-color: #4000c7;
    color: rgba(255, 255, 255, 1);
    transition: 0.3s;
  }

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

// Styled-Component로 Desc 컴포넌트의 CSS 구현
const Desc = styled.div`
  text-align: center;
`;

// 현재 어떤 Tab이 선
export const Tab = () => {

  // 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' },
  ];

  // parameter로 현재 선택된 인덱스 값을 전달하며, 이벤트 객체는 쓰지 않음
  const selectMenuHandler = (index) => {
    setCurrentTab(index)
  };

  return (
    <>
      <div>
      
      {/* map을 이용하면서 key 속성의 값으로 index를 넣어줌 */}
      {/* li 요소의 class명 : 선택된 tab은 'submenu focused', 나머지는 'submenu' */}
        <TabMenu>
          {menuArr.map((ele, index) => {
            return (
              <li
                key={index}
                className={currentTab === index ? 'submenu focused' : 'submenu'}
                onClick={() => selectMenuHandler(index)}
              >
                {ele.name}
              </li>
            );
          })}
        </TabMenu>
        <Desc>
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

 

Tag 컴포넌트 제작

tag 구현

1. Styled Component를 통해 컴포넌트의 CSS 구현

2. useState Hook을 사용하여, 태그 값들을 엘리먼트로 가지는 배열을 state로서 관리

3. 태그의 x를 누루면 해당 태그가 삭제하는 함수 구현 : filter 메소드로 해당 인덱스의 요소를 제외시키기

4. tag 배열에 새로운 태그 추가하는 함수 구현 : 이벤트 객체 사용

 

코드 작성

- target.event.value 값을 사용하기 위해서는 input 엘리먼트에 value 속성이 있어야 함

- setState 함수는 immutable해야 하므로, push가 아닌 concat 메소드 사용하기

 

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

// Styled-Component로 tag를 꾸밈
export const TagsInput = styled.div`
  생략
  
  > ul {
    생략

    > .tag {
      생략
      > .tag-close-icon {
        생략
      }
    }
  }

  > input {    
    생략
  }
  }

  &:focus-within {
    border: 1px solid #4000c7;
  }
`;

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

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

  // 태그를 삭제하는 메소드 구현
  const removeTags = (indexToRemove) => {
    setTags(tags.filter((ele, idx) => idx !== indexToRemove))
  };
  
  // 새로운 태그를 추가하는 메소드
  const addTags = (event) => {
    // 이미 입력되어 있는 태그라면 추가하지 않기 : includes 메소드로 판별
    // 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
    
    if (event.code=== 'Enter') {
      if (!(tags.includes(event.target.value)) 
          && !(event.target.value===null) 
          && !(event.target.value==='')) {
          
    setTags(tags.concat([event.target.value]))  // push가 아닌 concat 메소드 사용
    
    // 태그가 추가되면 input 창 비우기
    event.target.value=null;
        }
      }
    }
  
  return (
    <>
      <TagsInput>
        <ul id='tags'>
          {tags.map((tag, index) => (
            <li key={index} className='tag'>
              <span className='tag-title'>{tag}</span>
              
              {/* tag-close-icon이 tag에 x가 표시되도록 하고, 이것을 클릭하면 태그 삭제 /*}
              <span className='tag-close-icon' 
                onClick={()=> {removeTags(index)}}>
              </span>
            </li>
          ))}
        </ul>
        
        {/* input 엘리먼트를 통해 엔터를 누루면 태그가 입력되게 함 (onKeyUp) /*}
        <input
          className='tag-input'
          type='text'
          onKeyUp={(event)=> {addTags(event)}}
          placeholder='Press enter to add tags'
          value={null}
        />
      </TagsInput>
    </>
  );
};

 

레퍼런스 코드

- 여기서 input 엘리먼트에 value 속성이 없어도 event.target.value 사용이 가능했음 (더 알아보기)

- 엘리먼트 확인 시 나는 includes 메소드를 사용했고 여기서는 filter 메소드 사용

- 배열의 엘리먼트를 추가할 때 나는 concat 메소드를 사용했고 여기서는 spread 문법 사용

- if문 안의 내용은 레퍼런스 코드가 더 간단

 

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

  const [tags, setTags] = useState(initialTags);
  
  // 태그를 삭제하는 메소드 구현
  const removeTags = (indexToRemove) => {
    setTags(tags.filter((_, index) => index !== indexToRemove));
  };

  // 새로운 태그를 추가하는 메소드
  const addTags = (event) => {
    // 이미 있는 태그라면 추가하지 않기 : filter 메소드로 판별
    // 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 않기
    
    const filtered = tags.filter((el) => el === event.target.value);
    if (event.target.value !== '' && filtered.length === 0) {
      setTags([...tags, event.target.value]);     // spread 문법 사용
      
      // 태그가 추가되면 input 창 비우기
      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' 
          
          {/* 조건부 렌더링으로 'Enter'키 사용 여부 판별 */}
          onKeyUp={(event) => (event.key === 'Enter' ? addTags(event) : null)}
          placeholder='Press enter to add tags' 
        />
      </TagsInput>
    </>
  );
};

 

반응형

댓글