ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Stack-Overflow 클론] 위젯, 로그인, 회원가입 UI 구현하기
    Studying/Proj 과정 2023. 6. 18. 02:05

    개요

    Stack Overflow 웹사이트 클론 코딩 프로젝트에서 UI를 구현하는 과정을 기록합니다. 새로 배운 라이브러리와 TypeScript를 적용하느라 여러 시행착오가 있었습니다.

     

     

    나의 역할

    • 메인 페이지 오른쪽 사이드바(재사용 컴포넌트)
    • 로그인 페이지
    • 회원가입 페이지

     

    아무 생각 없이 요소들을 나열하면 금방 만들 정도의 단순한 UI이지만, 최근에 styled-components를 배우기도 했고 재사용 컴포넌트에 재미를 붙인 만큼 최대한 쓸모있게 만들어보고 싶었습니다.

     

     

     

    메인 페이지 RightSideBar

    아래 화면은 스택오버플로우의 오른쪽 사이드 바입니다. 비슷한 모양의 위젯이 줄지어 있습니다.

    Right Side Bar

     

    일단 RightSideBar 컴포넌트를 만들기 위해 사용된 파일은 아래와 같습니다.

     

    • RightSideBar.tsx - 오른쪽 사이드바 컴포넌트
    • Widget.tsx - 위젯 컴포넌트
    • BasicWidgetItem.tsx - 베이직 위젯 컴포넌트 아이템
    • LightWidgetItem.tsx - 라이트 위젯 컴포넌트 아이템
    • Collective.tsx - Collectives 위젯 아이템
    • SideBarAd.tsx - 광고 컴포넌트
    • msgs.json - 홈페이지 고정 문구
    • Container.styled.tsx - 자주 쓰는 컨테이너 Styled
    • 각종 SVG 아이콘 파일 - 아이콘.tsx 형식의 아이콘 파일

     

     

    컴포넌트 분리하기

    가장 먼저 재사용 가능한 컴포넌트를 분리했습니다. RightSideBar 를 보면 크게 4가지로 구성되어 있습니다.

     

    위젯(네모상자), 광고, Tags, Hot Network Questions 입니다.

     

    기능 별로 컴포넌트를 분리한 모습(위젯, 광고, 태그, Hot Network Questions)

     

    위젯

    Basic 위젯(좌) / Light 위젯(우)

     

    위와 같은 네모난 박스를 위젯이라 부르겠습니다. 저는 이 위젯을 Basic 위젯, Light 위젯 2가지로 분류했습니다.

     

    이유는 아래와 같은 차이점 때문이었습니다.

     

    • Basic 컴포넌트의 경우 Widget이 margin 없이 붙어있다.
    • Basic 컴포넌트 아이템은 특정 레이아웃을 가진다.
    • Basic 컴포넌트와 Light 컴포넌트 제목 폰트 크기, 스타일이 다르다.

     

    하지만 이렇게 나눈다 한들 "위젯" 이라면 무조건 가지는 공통된 부분이 있습니다.

     

    위젯 컨테이너

    바로 위젯 컨테이너입니다. Basic 위젯과 Light 위젯은 공통된 생김새의 컨테이너 레이아웃을 가집니다. 테마 색상이나 제목 폰트만 다를 뿐입니다. 그래서 일단 공통된 위젯 컨테이너를 Widget.tsx 컴포넌트로 분리한 뒤 재사용 하기로 했습니다.

     

    결론적으로, 아래 세 개의 컴포넌트를 만들어야 했습니다.

     

     

    위젯 컨테이너에서 재사용 가능한 컴포넌트들

     

     

    구현 과정

    위젯 컨테이너

     

    위젯 컨테이너는 id, type(Basic, Light), title, isStuck, children을 인자로 받습니다.

    • idx - Basic 위젯의 경우 위젯 간 간격이 없이 붙어있습니다. 따라서 가장 맨 위에 놓이는 위젯의 border-radius만 둥글게 만들고 아래에 따라붙는 위젯의 border-radius는 0이어야 합니다. map으로 호출된 위젯 컴포넌트 각각의 id 값을 인자로 받아 1번째 위젯인지 아닌지 구분하여 스타일을 적용하였습니다.
    • type - Baisc / Light 타입을 구분할 문자열.
    • title - 위젯 헤더 영역에 들어갈 제목.
    • isStuck - Basic 위젯이라도 붙지 않는 경우가 있으므로(질문 작성 페이지의 경우 분리되어 있다) 위젯을 붙일지 말지 결정하는 boolean.
    • children - 위젯 헤더 아래 내용 영역에 들어갈 컴포넌트.

     

     

    styled.div id는 사용자 지정 props로 사용할 수 없다?

    가장 처음 직면한 문제입니다.

    이어붙은 위젯의 border radius 확인

     

    위에서 설명했듯 Basic 위젯은 간격이 없이 붙어있어서 가장 첫 번째 위젯의 border-radius만 3px 이어야 했습니다. 너무 작은 숫자라 그런지 티는 안 나지만 아예 뾰족한 것과는 많이 달랐습니다.

     

    그래서 map으로 호출되는 Widget 중 '가장 첫 번째 위젯이 무엇인지' 알기 위해 <Widget> 컴포넌트에 id props로 map 메서드의 index값을 전달받았습니다.

     

    그런데 이상한 점이 있었습니다. <Widget> 컴포넌트를 구성하는 <WidgetContainer> 스타일드 컴포넌트가 id 값을 인식하지 못한 것입니다.

     

    여기에 관한 더 자세한 문제와 해결방법은 아래 게시물에 정리했습니다.

     

     

    Styled-Component에서는 id props를 사용할 수 없나요?

    문제의 발단 https://all-done.tistory.com/110 Stack-Overflow 홈페이지 클론 코딩 중, 오른쪽 사이드 바 UI를 구현하던 중 styled-component의 id props가 인식되지 않는 문제가 발생했습니다. React+TypeScript+StyledCompone

    all-done.tistory.com

     

     

     

    위젯 데이터 관리하기

    위젯을 만들면서 가장 큰 고민은 '위젯 데이터를 어떻게 관리할까?' 였습니다. 실제 홈페이지의 경우 핫 토픽이나 태그에 관련해선 실제 쌓여있는 데이터를 가져와 정렬해서 보여주겠지만, 저희는 그만큼의 데이터가 존재하지 않았습니다.

     

    따라서 그냥 홈페이지에 적힌 글자를 그대로 베껴올 수밖에 없었습니다. 다만 무작정 tsx 파일에 하드코딩 하고싶진 않았습니다.

     

    그때 문득 생각난 것이 WebKIT640 프로젝트였습니다. WebKIT640은 관리자 계정이 메인 페이지를 관리하는 기능이 존재했습니다. 부트캠프 모집 날짜, 발표일 등을 매 기수마다 바꿔야 했기 때문입니다.

     

    그와 비슷하게 스택오버플로우의 위젯 내용도 언제나 변경할 수 있었으면 좋겠다고 생각했습니다. 위젯과 관련된 코드 파일에 들어가서 글자를 수정하는 그런 식이 아니라, 언제든 간편하게 위젯 데이터를 추가하면 추가한대로 출력되었으면 좋을 것 같습니다.

     

    그래서 위젯 데이터를 담고있는 msgs.ts 파일을 만들었습니다. msgs.ts 파일은 아래와 같이 생겼습니다.

     

    msgs.ts

    만약 관리자 계정이 있다면 위젯 내용도 이리저리 바꿀 수 있을 것이라 생각하고 더미 데이터를 만들었습니다.

     

    RightSideBar.tsx 파일은 항상 RSideBarWidgetData를 불러와서 <Widget> 컴포넌트를 생성하기 때문에 홈페이지에 위젯을 추가/삭제 하고싶으면 해당 데이터만 조작해주면 됩니다.

     

     

     

    SVG 이미지 파일 클론 코딩하기

    위젯 아이템에 들어가는 연필 모양 SVG 아이콘이 필요했습니다. 아이콘을 사용하기 위해 react-icons 라이브러리를 설치해도 괜찮냐고 여쭤봤다가 Tree Shaking이 안 되어서 번들 사이즈가 늘어난다고 기각되었습니다.

     

    2023. 07. 01

    + react-icons Tree Shaking 방법을 찾았습니다!!

     

    React Icons Imports everything even when included 2 or 3 icons · Issue #154 · react-icons/react-icons

    I had imported below icons in Create React App and I see that it included the whole library in the bundle. Attached is a screenshot. How do we get around this? import { FaBarChart, FaDatabase, FaMi...

    github.com

     

    아쉽지만 대신 더 간편한, 홈페이지에서 SVG 이미지를 가져오는 방법을 알게 되었습니다.

     

    바로 개발자 도구로 아이콘 요소를 찍어서 svg 태그를 복사/붙여넣기 하는 것입니다.

     

    연필 아이콘 요소를 찍었을 때 개발자도구 Elements

    그 뒤 아래와 같은 연필 아이콘 컴포넌트 파일을 만들어서 export한 뒤 사용하면 됩니다.

     

    import { SvgPropsType } from '@/types';
    
    interface PencilSMProps extends SvgPropsType {}
    
    const PencilSM = (attribute: PencilSMProps) => {
      return (
        <svg aria-hidden="true" className="va-text-top svg-icon iconPencilSm" width="7%" height="14" viewBox="0 0 7 14">
            <path d="m11.1 1.71 1.13 1.12c.2.2.2.51 0 .71L11.1 4.7 9.21 2.86l1.17-1.15c.2-.2.51-.2.71 
            0ZM2 10.12l6.37-6.43 1.88 1.88L3.88 12H2v-1.88Z"></path>
        </svg>
      );
    };
    
    export default PencilSM;

     

    이때 중요한 것은 SvgPropsType 입니다.

     

    <svg> 태그를 잘 보시면 여러가지 속성들이 있습니다. 만약 아이콘을 재사용할 때마다 속성들이 조금씩 달라야 한다면 어떡할지 고민해야 했습니다. 매번 다른 속성값의 아이콘 컴포넌트를 만들면 그건 재사용의 의미가 없어보였습니다.

     

    그렇다면 svg 태그의 속성들을 props로 받아 사용하면 훨씬 수월해질텐데, TypeScript는 타입을 정확히 명시해주어야 한다는 귀찮은 점이 있습니다. 그래서 svg 태그의 props 타입은 대체 무엇일지 생각했습니다.

     

    리액트에서 컴포넌트를 정의할 때, HTML 요소와 똑같은 속성 타입을 사용하도록 정의하는 게 좋습니다. HTML 엘리먼트를 사용할 때의 동작과 동일하게 만듦으로써 사용자의 자유도를 높여주는 겁니다.

     

    리액트는 이를 위해 HTML 엘리먼트 속성 타입들을 편하게 쓸 수 있도록 HttpAttributes 타입을 제공합니다.

     

    type SpanProps = HTMLAttributes<'span'>;
    const Text = (props: SpanProps) => { /* ... */ }

     

    이렇게 하면 <Text> 컴포넌트는 HTML의 span 태그가 사용하는 속성 타입들을 가질 수 있게 됩니다. 제네릭 변수에 원하는 요소 이름을 적으면 해당 요소의 속성들을 제공합니다.

     

    하지만 HtmlAttributes는 "리액트에서 제공하는 HTML 기본 속성들"만 가지고 있기 때문에 refkey를 사용하지 못 합니다. DetaildHTMLProps 라는 타입을 사용하면 ref props를 포함할 수 있다지만 말 그대로 타입만 선언될 뿐 컴포넌트가 ref를 통과시키지 않는다고 합니다.

     

    ref props는 리액트에서 만큼은 다르게 핸들링 되어야 한다고 공식문서에도 적혀있습니다.

     

    결국 리액트 컴포넌트가 "완벽하게 HTML 요소를 재현하려면" 상위 컴포넌트에서 ref props를 받아서 하위 컴포넌트로 전달하는 기능을 만들어야 합니다. 이때 사용되는 함수가 forwardRef 입니다.

     

    const Text = forwardRef(function Text(
      props: ComponentPropsWithoutRef<'span'>,
      ref: Ref<HTMLSpanElement>
    ) {
      return <span ref={ref}>{props.children}</span>
    });

     

    이렇게 <Text> 컴포넌트를 forwardRef로 감싸주면 ref를 전달 받아 span 요소에 전달해줄 수 있습니다. 전달된 레퍼런스를 뜻하는 ref는 별도의 인자로 들어와 props와 분리하여 정의합니다.

     

    이때 ref를 제외한 나머지 프로퍼티, props 의 타입이 바로 ComponentPropsWithoutRef 입니다.

     

    이거 하나 말하려고 서론이 길었네요. 그래도 탄생 과정처럼 설명해야 이해가 잘 가서 어쩔 수 없다고 생각합니다.

     

    즉 결론적으로 svg 태그에 ref 속성을 제외한 모든 속성 타입들을 정의한 게 ComponentPropsWithoutRef<'svg'> 입니다.

     

    <svg> 태그를 반환하는 <PencilSM> 아이콘 컴포넌트에 인자로 <svg> 요소의 속성값을 전해주려면 인자 타입을 ComponentPropsWithoutRef<'svg'> 로 해야하는 겁니다.

     

    근데 홈페이지를 클론하면서 여러 종류의 SVG 아이콘를 클론하게 될 것이고, 그때마다 저 긴 이름의 타입을 명시하는 건 귀찮을 뿐더러 가독성을 해치기 때문에 별도로 아래와 같이 interface를 정의해줍니다.

     

    import { ComponentPropsWithoutRef } from 'react';
    
    export interface SvgPropsType extends ComponentPropsWithoutRef<'svg'> {}

     

    그 후, 다양한 아이콘 컴포넌트를 생성할 때마다 다음과 같이 import 하여 svg 요소의 속성값을 인자로 받아 어떤 스타일이든 재사용 가능한 아이콘 컴포넌트를 정의할 수 있게 되었습니다.

     

    import { SvgPropsType } from '@/types';
    import React from 'react';
    
    interface InformationIconProps extends SvgPropsType {}
    
    const InformationIcon = (attribute: InformationIconProps) => {
      return (
        <svg aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" {...attribute}>
          <path d="M7 1a6 6 0 1 1 0 12A6 6 0 0 1 7 1Zm1 10V6H6v5h2Zm0-6V3H6v2h2Z"></path>
        </svg>
      );
    };
    
    export default InformationIcon;

     

    저도 그 결과물로 아래와 같이 귀여운 연필 아이콘을 무사히 가져올 수 있었습니다.

     

    BasicWidgetItem

    참고🎁 리액트에서 HTML 요소 흉내내기(feat.TypeScript props 타입)

     

     

     

    SVG 파일 Flex 안에서 비율 조정하기

    두 번째 문제에 직면했습니다. SVG 아이콘은 width, height 을 지정해주고 flex-basis:auto로 하여도 공간 넓이에 따라 비율이 달라집니다. 그래서 아래 텍스트 공간에 따라 비율이 멋대로 뒤죽박죽이 되었습니다.

    SVG 연필 아이콘 비율이 뒤죽박죽인 모습

     

    해결 방법으론 텍스트 공간 너비를 width:85%로 하여 연필 아이콘이 적당한 크기를 차지할 만큼 공간을 제공하는 것이었습니다.

     

    하지만 이와는 다른 문제로 또 한 번 아이콘 크기 문제가 있었습니다.

     

    대충 그림으로 그리자면 다음과 같습니다.

     

    문제 상황 재현

     

    물음표가 그려진 원형 아이콘을 사용하려고 불러왔더니 알 수 없는 공간을 차지하는 이슈였습니다. 저는 아이콘의 크기를 width="14" height="14" viewBox="0 0 14 14" 로 지정했는데 바깥에 알 수 없는 여백이 존재했습니다. 마우스로 아이콘 요소만 클릭해보면 14x14인데 저 여백 때문에 FlexBox 비율이 망가졌습니다.

     

    해결책으로는 width="14%" height="14" viewBox="0 0 14 14" 같이 width만 %로 정한 것이었습니다.

     

    크기 문제 해결

     

    이렇게 하니까 빈 여백이 사라졌습니다. margin, padding도 아니던 그 공간은 대체 무엇이었는지 아직도 모르겠네요. height은 멀쩡히 나오는데 width에 그 여백이 생겼습니다.

     

     

    Favicon 클론하기

    다음 문제는 파비콘을 불러오는 데 있어서 발생했습니다.

     

     

    파비콘을 불러오지 못 한 모습

     

    SVG 아이콘이야 태그를 복사해 그대로 사용할 수 있지만, 이미지인 파비콘은 어떻게 가져와 사용하는지 알 수 없었습니다.

     

    어찌저찌 개발자도구 Sources 에서 StackOverflow에서 사용한 파비콘 이미지 파일을 찾긴 했는데, 그게 다음과 같이 생겼습니다.

     

    Stack-Overflow favicons img 파일

     

    왜 수많은 파비콘들이 일렬로 붙어있을까 당황스러웠습니다. 저걸 스택오버플로우에서는 어떻게 가져와 사용하는지 알아봤습니다.

     

    홈페이지에서 개발자도구를 열고 파비콘 하나를 클릭해보니 다음과 같은 스타일이 존재했습니다.

     

     

    favicon background-image

     

    바로 위에서 봤던 세로로 엄청 긴 파비콘 이미지 주소였습니다. 어떻게 된 일일까요. 저 긴 이미지를 background-image url로 넣었더니 원하는 아이콘 하나만 딱 나온 것입니다.

     

     

    background-position

     

    position에 관한 css 속성을 찾아보니 파비콘마다 background-position을 지정해 파비콘 크기 영역 만큼만 잘려 보이도록 만든 것이었습니다.

     

    덕분에 파비콘을 그대로 가져와 클론할 수 있었습니다.

     

     

     

    홈페이지 이미지 클론하기

    이미지 클론도 파비콘과 똑같습니다. 이미지 요소를 클릭한 뒤 Styles 에서 url을 검색하면 background-image:url() 을 쉽게 찾을 수 있습니다.

     

    background-image

    그 다음 url에 마우스를 hover 하면 다음과 같이 cdn 이미지 링크가 나타납니다.

     

     

    저 링크를 복사하여 똑같이 사용하면 군더더기 없이 똑같은 클론 코딩이 가능합니다.

     

    클론 결과

     

     

     

    styled-components에서 SVG 이미지 CSS 조정하기

    SVG 이미지에 마우스 hover 시 색상 변경해야합니다.

     

    다음과 같이 SVG 아이콘을 Wrapper styled-component로 감싸줍니다.

    const SideBarAd = ({url, src}:SideBarAdProps) => {
        return(
            <div className='mb-3'>
                <ImgContainer href={url}>
                    <IconWrapper>
                        <XIcon/>
                    </IconWrapper>
                    <AdImage src={src}/>
                </ImgContainer>
                ...
            </div>
        )
    }

     

    스타일드 컴포넌트에서 다음과 같이 자식 svg 요소에 접근해 css를 적용했습니다.

    const IconWrapper = styled.div`
        ...
        & svg {
            fill: #00aecd;
            background-color: white;
            stroke: #00aecd;
            stroke-width:1.25px;
        }
    
        & svg:hover {
            background-color: #707070;
            stroke: white;
        }
    `;

     

     

     

    img 요소에 url을 넣으면 테두리가 생긴다.

    img 요소에 링크를 넣거나, background-image:url로 이미지를 불러오면 테두리가 생긴다는 사실을 까먹었습니다.

     

    이미지 링크를 복사해 클론하다가 원치 않는 테두리가 생겨 엄청 스트레스 받았습니다. background-image: url() 의 경우 border:0;을 해도 안 없어졌습니다.

     

    img 요소에 링크를 넣었을 때 생기는 테두리는 border:0; 으로 지웠다는 분들이 많은데 저는 왜인지 적용이 안 되어서 그냥 div태그로 바꾼 뒤 background-img를 적용했습니다.

     

     

     

    여러 줄의 텍스트에 ... 축약 css 넣는 법

    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2; // 텍스트를 자르는 단위(2줄)
    -webkit-box-orient: vertical;
    word-wrap: break-word;

     

     

     

     

    로그인 페이지

    로그인 페이지

     

     

     

     

    회원가입 페이지

    회원가입 페이지

     

     

     

    최종 결과

    배포 서버는 최종 완료 후 이틀 정도만 열어두신다고 해서, UI만 확인 가능합니다. 아쉬운 점은 다른 팀원들이 만든 페이지 중 로그인 해야 볼 수 있는 페이지가 있다(회원정보조회 등)는 점입니다... 

     

    다함님이 다한 프로젝트

     

    dahamoverflow.netlify.app

     

Designed by Tistory.