Diary/Retrospective

[항해99 / TIL] 🚢 23-04-29 Styled Components로 화면 요소 만들기

Olivia Kim 2023. 4. 29. 15:48
반응형

 

요구사항 정의

예시 화면

 

구현해야 할 기능

Button

  • styled-components를 이용해 구현하며, props를 적절하게 활용할 것
  • 버튼 label에 선택적으로 아이콘을 넣을 수 있도록 구현

 

 

Modal

  • 취소, 확인이 있고, overlay 클릭 시 모달이 닫히지 않음
  • 닫기 버튼만 있고, overlay 클릭 시 모달이 닫힘
  • 모달을 on 시키는 버튼의 형태는 각각 달라야 하며 위에서 만든 버튼 재사용 가능

 

 

Inpnut

  • 일반 형식의 Input
  • 숫자를 넣었을 때 3자리 숫자마다 ,가 찍히는 input
  • form을 구현하고 각 inpnut에 값 입력 후 저장 버튼 클릭 시 { name: '아무 텍스트', price: '콤마가 없는 금액' } 을 alert에 표시

 

 

Select

  • select 클릭 시 option들이 나오고 해당 option 클릭 시 select 값 변경
  • 부모 요소에 overflow: hidden이 있더라도 자식 컴포넌트가 부모 컴포넌트를 넘어갔을 때 가려지지 않아야 함

 

 

 


컴포넌트 분리 및 역할 분담

구현 및 실행 화면

 

📦src
 ┣ 📂components
 ┃ ┣ 📜Button.jsx       : Button을 모듈화한 컴포넌트 (최종 작업물 기준 본인 담당)
 ┃ ┣ 📜Buttons.jsx      : 버튼을 불러오기 위한 컴포넌트
 ┃ ┣ 📜Input.jsx        : Input 컴포넌트
 ┃ ┣ 📜Modal.jsx        : Modal 컴포넌트
 ┃ ┗ 📜Select.jsx       : Select 컴포넌트 (최종 작업물 기준 본인 담당)
 ┣ 📜App.jsx
 ┣ 📜index.js
 ┗ 📜reportWebVitals.js

 

 

 


중점을 둔 사항

Button

function Button({ size, color, icon, onClick, children }) {
  const Button = styled.div`
    ${() => colorHandler(color)};
    ${() => sizeHandler(size)};
    border-radius: 10px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 13px;
    cursor: pointer;
    &:active {
      filter: brightness(70%);
    }
  `;
  
  const colorHandler = (color) => {
    switch (color) {
      case 'primary':
        return `border: 3px solid #55EFC4; background-color: #55EFC4`;
      case 'negative':
        return `border: 3px solid #FAB1A0; color: #D63031; background-color: #FAB1A0`;
    }
  };

  const sizeHandler = (size) => {
    switch (size) {
      case 'large':
        return `width: 185px; height: 45px; background-color: white; font-weight: bold;`;
      case 'medium':
        return `width: 120px; height: 40px;`;
      case 'small':
        return `width: 90px; height: 35px;`;
    }
  };


  return (
    <>
      <Button
        onClick={onClick}
      >
        { children }&nbsp;{ icon }
      </Button>
    </>
  )
}

export default Button

 

버튼 구현 시  props 키워드에 따라 세 가지 사이즈, 두 가지 색상을 선택해서 버튼을 가져올 수 있도록 구현했다.

 

 

 

Select

function Select() {
  const languageList = ['리액트', '자바', '스프링', '리액트네이티브'];
  const [language, setLanguage] = useState('리액트');
  const [showList, setShowList] = useState(false);

  const toggleShowList = () => setShowList(!showList);
  const liClickHandler = (index) => {
    setLanguage(languageList[index]);
    toggleShowList();
  };

  const selectWrapRef = useRef();
  useEffect(() => {
    const clickListOutside = (e) => {
      if (selectWrapRef.current && !selectWrapRef.current.contains(e.target)) {
        toggleShowList();
      }
    };
    document.addEventListener('mousedown', clickListOutside);
    return () => {
      document.removeEventListener('mousedown', clickListOutside);
    };
  }, []);

  return (
    <>
      <Wrap>
      <h1>Select</h1>
        <SelectButton
          onClick={toggleShowList}
        >
          {language}
          <FontAwesomeIcon icon={faCaretDown} />
        </SelectButton>
        {
          showList &&
          <div
            ref={selectWrapRef}
          >
            <LanguageUl>
            {
              languageList.map((item, index) => {
                return (
                  <LanguageLi
                    key={index}
                    onClick={() => liClickHandler(index)}
                  >
                    { item }
                  </LanguageLi>
                )
              })
            }
            </LanguageUl>
          </div>
        }
      </Wrap>
    </>
  )
}

const Wrap = styled.div`
  border: 3px solid lightgrey;
  margin-top: 30px;
  overflow: hidden;
`;

const SelectButton = styled.button`
  width: 245px;
  height: 40px;
  padding: 0 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  background-color: white;
  border: 1px solid lightgrey;
  border-radius: 10px;
  cursor: pointer;
`;

const LanguageUl = styled.ul`
  width: 245px;
  height: 40px;
  margin: 0;
  padding-left: 0;
  list-style: none;
  position: absolute;
  `;

const LanguageLi = styled.li`
  height: 40px;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: white;
  border-left: 1px solid lightgrey;
  border-right: 1px solid lightgrey;
  font-size: 13px;
  cursor: pointer;
  &:hover {
    background-color: lightgrey;
    border-radius: 5px;
  }
  &:first-child {
    border-top: 1px solid lightgrey;
    border-radius: 5px;
  }
  &:last-child {
    border-bottom: 1px solid lightgrey;
    border-radius: 5px;
  }
`;

export default Select

 

예시 화면에서 select 기본 값이 리액트로 되어있었기 때문에 useState의 기본값을 '리액트'로 주었다. 이후 사용자가 클릭한 각 li 값을 감지하여 노출할 state값을 변경해 주었다.

 

 

부모 요소에 overflow: hidden이 있더라도 자식 컴포넌트가 가려지지 않도록 position: absolute값을 주었고, 사용자가 select 외부의 값을 클릭했을 때 select box가 닫히도록 true, false값과 useEffect를 활용하여 mousedown을 감지하는 이벤트를 설정했다.

 

 

 

Input

function Input(props) {
  const [name, setName] = useState('');
  const [price, setPrice] = useState(0);

  const nameChangeHandler = (e) => setName(e.target.value);
  const priceChangeHandler = (e) => {
    const priceNum = Number(e.target.value.replace(/,/g, ''));
    if (!Number.isNaN(priceNum)) {
      setPrice(priceNum.toLocaleString());
    }
  }

  const saveBtnClickHandler = (e) => {
    e.preventDefault();
    if(name === '') {
      alert(`이름과 가격 모두 입력해주세요.`);
      return;
    }
    alert(`{ name: ${name}, price: ${price.replace(/,/g, '')} }`);
  }

  return (
    <>
      <h1>Input</h1>
      <section>
        <div>
          <Form>
            <label htmlFor="name">
              이름 &nbsp;
              <UserInput
                type="text"
                id="name"
                value={name}
                onChange={nameChangeHandler}
              />
            </label>
            <label htmlFor="price">
              가격 &nbsp;
              <UserInput
                type="text"
                id="price"
                value={price}
                onChange={priceChangeHandler}
              />
            </label>

            <SmallBtn
              onClick={saveBtnClickHandler}
              color={props.color}
            >
              저장
            </SmallBtn>
          </Form>
        </div>
      </section>
    </>
  )
}

const Form = styled.form`
  display: flex;
  gap: 30px;
`;

const UserInput = styled.input`
  width: 180px;
  height: 40px;
  padding: 0 10px;
  border: 1px solid black;
  border-radius: 10px;
`;

export default Input

 

사용자가 금액 input에 isNaN이 true일 경우만 setPrice를 하면서 toLocaleString()을 이용해 세 자리 수마다 , 를 찍어 화면에 출력시켰다. 또한 e.target.value값을 감지할 때는 replace로 , 를 제거해 값을 가져올 수 있도록 했다.

 

 

 

Modal - Quote

function ModalQuote(props) {
  return (
    <BackgroundDiv>
      <Section>
        <p>
          닫기와 확인 버튼 2개가 있고, 
          외부 영역을 눌러도 모달이 닫히지 않아요.  
        </p>
        <BtnAreaDiv>
          <SmallBtn
            color={props.color}
            onClick={props.toggleModalQuote}
          >
            닫기
          </SmallBtn>
          <SmallBtn>
            확인
          </SmallBtn>
        </BtnAreaDiv>
      </Section>
    </BackgroundDiv>
  )
}

const BackgroundDiv = styled.div`
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  position: fixed;
  background-color: rgba(228,228,228,0.8);
`;

const Section = styled.section`
  width: 460px;
  height: 260px;
  padding: 20px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
  border-radius: 15px;
`;

const BtnAreaDiv = styled.div`
  display: flex;
  justify-content: flex-end;
  gap: 5px;
`;

export default ModalQuote

 

 

 

Modal - Single

function ModalSingle(props) {
  const divNode = useRef();
  const divClickHandler = (e) => {
    if(e.target === divNode.current) {
      props.toggleModalSingle();
    }
  }
  
  return (
    <BackgroundDiv
      ref={divNode}
      onClick={divClickHandler}
    >
      <Section>
        <p>
          닫기와 확인 버튼 1개가 있고, <br /> 
          외부 영역을 누르면 모달이 닫혀요.  
        </p>
        <CloseButton
          onClick={props.toggleModalSingle}
        >
          X
        </CloseButton>
      </Section>
    </BackgroundDiv>
  )
}

const BackgroundDiv = styled.div`
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  position: fixed;
  background-color: rgba(228,228,228,0.8);
`;

const Section = styled.section`
  width: 300px;
  height: 200px;
  padding: 20px;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
  border-radius: 15px;
`;

const CloseButton = styled.button`
  width: 30px;
  height: 30px;
  margin-top: 10px;
  margin-left: 10px;
  border: none;
  border-radius: 50%;
  cursor: pointer;
`;

export default ModalSingle

 

모달에서 모달 외부영역 클릭 시 모달을 닫는 토글 이벤트가 실행되도록 다음과 같이 코드를 구성했다.

 

 

const divNode = useRef();
const divClickHandler = (e) => {
    if(e.target === divNode.current) {
    	props.toggleModalSingle();
    }
}

 

1. 모달 외부에 레이아웃을 하나 깔아두고 그곳에 ref를 걸어준다.

2. ref를 건 영역에 onClick 이벤트를 주고

3. 클릭된 공간이 바깥 공간이 맞으면 조건문으로 토글 확인하여 모달 토글 함수를 실행한다.

 

 

 

 

[리팩토링 후(최종 제출본)] https://github.com/hansololiviakim/react-styled-components-after

 

GitHub - hansololiviakim/react-styled-components-after: 항해99 14기 리액트주차 스타일드 컴포넌트 실습 (리팩

항해99 14기 리액트주차 스타일드 컴포넌트 실습 (리팩토링 후). Contribute to hansololiviakim/react-styled-components-after development by creating an account on GitHub.

github.com

 

 

[리팩토링 전(최초 작성본)] https://github.com/hansololiviakim/react-styled-components-before

 

GitHub - hansololiviakim/react-styled-components-before: 항해99 14기 리액트주차 스타일드 컴포넌트 실습 (리팩

항해99 14기 리액트주차 스타일드 컴포넌트 실습 (리팩토링 전). Contribute to hansololiviakim/react-styled-components-before development by creating an account on GitHub.

github.com

 

 

 


트러블슈팅

section 태그 안에서 h1의 크기가 작아지는 문제

브라우저의 디폴트 스타일 시트값 때문이다. h1 태그를 꼭 section 태그 안에 넣어야 하는 상황이 아니었기 때문에 section 태그 외부에서 h1 태그를 사용했다. 추후에는 reset.css를 이용해야겠다.

 

 

 

배경 색에만 opacity 효과주되 자식 요소에는 영향이 없도록 지정하는 법

방법 1. rgba로 배경 색에만 색상을 조정한다.

background-color: rgba(228,228,228,0.8);

 

방법 2. CSS의 ::after 가상 선택자를 사용한다.

 

 

 


질의사항

기술매니저님과의 질의

❓ 버튼들이 있는 Button.jsx에서 휴먼 에러를 줄이기 위해 단어들을 배열에 넣고 배열의 인덱스를 props로 내려주었는데 이렇게 하는 게 오히려 불편한 방법인지 궁금하다. 또한 지금과 같이 버튼을 컴포넌트화해서 사용하는 상황에서 프로젝트 규모가 커졌을 때는 어떻게 props를 내려주는지 궁금하다.

💡 (코드 리뷰 대기 중)

 

❓ select 영역 구현 시 select box 외부의 요소를 클릭했을 때 닫힐 수 있도록 useEffect와 addEventListener의 mousedown으로 구현했다. DOM을 직접적으로 조작하는 방법은 좋지 않다고 들었는데 현업에서도 이러한 방법을 사용하는지 다른 방법을 사용하는 게 좋을지 궁금하다.

💡 (코드 리뷰 대기 중)

 

❓ 리액트에서 숫자 세 자리마다 , 를 찍어 사용자에게 보여줄 때 useState로는 toLocaleString을 걸고, DB에 데이터를 저장할 때는 replace 등으로 , 를 떼고 DB에 저장하는지 아니면 , 가 붙어있는 상태로 DB에 저장하는지 궁금하다.

💡 (코드 리뷰 대기 중)

 


 

✅ 이번 과제에서는 4개의 요소를 2개씩 나눠 구현해 보고, 다음날 서로 안해본 요소 2개를 바꿔서 구현해보고, 코드리뷰를 한 다음 최종적으로 제일 좋다고 생각하는 코드를 제출하는 형식으로 진행했다. 단순 CSS 요소만 사용하는 과제인 줄 알고 금방 끝날 것이라 생각했는데 버튼을 props를 이용해 컴포넌트화 하는 것이나, 리액트 hook을 사용해 화면을 구현할 수 있도록 생각해 보는 시간이 생겼다.

 

스타일드 컴포넌트와는 전보다 좀 친해진 기분이 들지만 아직도 가독성 측면에서는 그렇게 좋아 보인다는 생각이 안 들어서 스타일드 컴포넌트가 어떤 장점이 있는지 찾아보았다. 스타일드 컴포넌트를 사용하는 것의 장점은 다음과 같다.

 

 

1. 컴포넌트 기반 스타일링

 👉🏼 스타일드 컴포넌트 사용 시 컴포넌트와 스타일을 함께 작성할 수 있어 코드의 가독성과 유지보수성을 높인다.

 

2. 동적 스타일링

 👉🏼 props, 상태 등과 같은 리액트의 기능을 활용해 동적 스타일링을 할 수 있다.

 

3. 스타일 유지 보수 용이성

 👉🏼 스타일을 작성하고 적용하는 데 필요한 모든 것을 한 곳에서 처리할 수 있다. 이때 스타일 코드의 중복을 줄이고 유지보수성을 향상시킬 수 있다.

 

4. 스타일의 범위 제한

 👉🏼 스타일드 컴포넌트를 사용하면 스타일이 전역적으로 적용되지 않도록 제한할 수 있다. 이는 스타일의 충돌을 방지하고 코드의 안정성을 높일 수 있다. (리액트에서 CSS를 사용할 경우의 단점과 반대된다고 한다.)

 

5. 서버 사이드 렌더링

  👉🏼 스타일드 컴포넌트는 서버 사이드 렌더링을 지원하므로 초기 로딩 속도를 높일 수 있다.

 

 

리액트에서 CSS를 사용할 경우 전역 스코프로 스타일을 적용하게 되면 스타일이 충돌할 수 있다는 점은 스타일드 컴포넌트의 장점을 찾아보면서 처음 알게 되었다. 결국 스타일드 컴포넌트를 사용한다는 것은 스타일도 모듈화 시킨다는 것이고 CSS-in-JS을 사용하는 이유라고 한다.

 

왜 스타일드 컴포넌트를 사용하는지 알게 되었으니 스타일드 컴포넌트를 효율적으로 사용하려면 글로벌 스타일을 적용하는 법을 더 연습해 봐야겠다.

 

 

 


[참고 자료]

https://stackoverflow.com/questions/64389523/h1-size-differs-in-div-tag-and-section-tag

 

h1 size differs in Div tag and section tag

I used H1 tag inside section tag and div tag, but h1 in section tag is small rather than h1 tag in div tag. Why? <section> <h1>Hello World</h1> </section> <h1>He...

stackoverflow.com

https://codingbroker.tistory.com/58

 

[HTML, CSS] opacity로 요소, 배경화면 투명하게(흐리게)하는 방법, 자식 요소에 같이 적용되는 문제

CSS의 opacity를 이용하여 HTML의 요소를 투명하게 만드는 방법과 자식 요소는 제외하고 배경화면만 투명하게 만드는 방법에 대해서 살펴보겠습니다. div와 그 자식 요소 h1이 있습니다. Hello 배경화

codingbroker.tistory.com

 

 

 

반응형