브런치 클론 프로젝트 _ 기억하고 싶은 코드

연정·2021년 11월 14일
0

Personal Projects

목록 보기
4/6
post-thumbnail

브런치 클론 프로젝트에서 나의 주요 담당 파트는 메인 페이지였다.
메인 페이지에는 UI적으로 구현할 부분이 많았는데,
그 중에서도 슬라이더 부분에 가장 많은 공을 들여 구현하였다.

어려웠던 점

  • 일단 슬라이더 자체를 구현해본 적이 없어 접근 방법을 알아내는 게 어려웠다.
  • 불규칙한 각 슬라이더별 구성으로 효율적인 코드를 짜기 어려웠다.
  • 슬라이더가 끝에 도착했을 때 더 가지 않고 멈추도록 구현해야 했다.
  • 좌우 버튼은 생성뿐 아니라 슬라이더의 양끝에 올 때 사라지는 걸 구현해야 했다.
  • 숫자 버튼과 좌우 버튼을 동기화 시켜야 했다.
  • 마지막으로 마우스 hover 시 이미지에 변화가 있어야 했다.

성장단계

1차 성장)
하드 코딩으로 어찌저찌 모양은 만듦.
그치만 코드가 너무 길어서 내가 봐도 무슨 코든지 알아볼 수 없없음

2차 성장)
모양이 같은 슬라이더를 모아 하나의 컴포넌트로 만듦.
총 4개의 구성이라 4개의 컴포넌트가 생김. 이제는 코드를 알아볼 수는 있음

3차 성장)
디자인 구성 상관없이 하나의 컴포넌트로 전체를 커버.
html은 매우 간결해졌으나, 디자인의 한계로 css는 조금 복잡

작성한 코드

슬라이더 구현

  • 보여지는 부분을 관장하는 div 태그를 생성하고 width & height를 고정
  • div 태그 하위에 있는 콘텐츠 부분의 width를 보여지는 부분 * 슬라이더 개수로 계산하여 너비 설정을 하고, grid를 활용해 각각의 콘텐츠(컴포넌트)의 위치를 설정
  • state값으로 slide positionactiveBtn을 설정하고 state 값을 변화시키는 함수를 제작하여 이동 위치 설정
  • transform을 활용하여 이동기능까지 구현 + transition으로 스피드 조절

좌우로 이동할 때 어디에 마이너스 값을 줘야하는지 너무 헷갈려서 한참 헤맸다..

버튼 구현

  • activeBtn의 경우 코드는 잘 작성했으나 작동을 하지 않아 한참 고민했는데, Number인지 String인지의 차이로 값이 제대로 저장되지 않아서 였다!
  • render 내에 슬라이드가 처음인지와 끝인지를 판별하는 테스트를 추가하여 boolean값에 따라 좌우버튼이 숨겨지거나 보이도록 구현
[React]

import React from 'react';
import SlideContent from './SlideContent';
import './LikedContents.scss';

class LikedContents extends React.Component {
  constructor() {
    super();

    this.state = {
      slidePosition: 0,
      activeBtn: '1',
    };
  }

  slideSize = 970;
  hiddenSlideLength = 3;

  handlePrevBtn = e => {
    const { slidePosition, activeBtn } = this.state;
    const { slideSize } = this;

    if (slidePosition >= 0) {
      this.setState({ slidePosition: 0 });
    } else {
      this.setState({
        slidePosition: slidePosition + slideSize,
        activeBtn: (Number(activeBtn) - 1).toString(),
      });
    }
  };

  handleNextBtn = e => {
    const { slidePosition, activeBtn } = this.state;
    const { slideSize, hiddenSlideLength } = this;

    if (slidePosition <= slideSize * -1 * 3) {
      this.setState({ slidePosition: slideSize * -1 * hiddenSlideLength });
    } else {
      this.setState({
        slidePosition: slidePosition - slideSize,
        activeBtn: (Number(activeBtn) + 1).toString(),
      });
    }
  };

  moveToSlide = e => {
    const { slideSize } = this;
    const { value } = e.target;

    this.setState({
      slidePosition: value * slideSize * -1 + slideSize,
      activeBtn: value.toString(),
    });
  };

  render() {
    const { slidePosition, activeBtn } = this.state;
    const { slideSize, hiddenSlideLength } = this;
    const { slideContents } = this.props;

    const isSlideEnd = slidePosition === slideSize * -1 * hiddenSlideLength;
    const isSlideStart = slidePosition === 0;

    return (
      <section className="likedContents">
        <div className="articleWrapper">
          <div
            className="slideDisplay"
            style={{
              transform: `translateX(${slidePosition}px)`,
            }}
          >
            <div className="slideArticles">
              {slideContents.map((content, idx) => {
                return <SlideContent key={idx} slideContent={content} />;
              })}
            </div>
          </div>

          <button
            className={isSlideStart ? 'prevBtn hidden' : 'prevBtn'}
            onClick={this.handlePrevBtn}
          >
            <i class="fas fa-chevron-left" />
          </button>
          <button
            className={isSlideEnd ? 'nextBtn hidden' : 'nextBtn'}
            onClick={this.handleNextBtn}
          >
            <i class="fas fa-chevron-right" />
          </button>
        </div>

        <ul className="contentsOrder">
          {CONTENT_ORDER_DATA.map((data, idx) => {
            return (
              <li
                key={idx}
                value={data.value}
                onClick={this.moveToSlide}
                className={
                  activeBtn === `${data.value}` ? 'numBtn active' : 'numBtn'
                }
              >
                {data.text}
              </li>
            );
          })}
        </ul>
      </section>
    );
  }
}

const CONTENT_ORDER_DATA = [
  { value: '1', text: '01' },
  { value: '2', text: '02' },
  { value: '3', text: '03' },
  { value: '4', text: '04' },
];

export default LikedContents;
[SCSS]

@import '../../../styles/variables.scss';

.likedContents {
  margin-top: 60px;

  .articleWrapper {
    position: relative;

    .slideDisplay {
      width: 970px;
      height: 500px;
      margin: 0 auto;
      transition: all 400ms ease-in-out;

      .slideArticles {
        @include grid(grid, repeat(24, 1fr), 0, repeat(6, 1fr));
        width: calc(970px * 4);
        height: 100%;

        div:nth-of-type(1) {
          grid-column: 1 / 4;
          grid-row: 1 / 7;
        }

        div:nth-of-type(2) {
          grid-column: 4 / 7;
          grid-row: 1 / 4;
        }

        div:nth-of-type(3) {
          grid-column: 4 / 7;
          grid-row: 4 / 7;
        }

        div:nth-of-type(4) {
          grid-column: 7 / 9;
          grid-row: 1 / 7;
        }

        div:nth-of-type(5) {
          grid-column: 9 / 11;
          grid-row: 1 / 7;
        }

        div:nth-of-type(6) {
          grid-column: 11 / 13;
          grid-row: 1 / 7;
        }

        div:nth-of-type(7) {
          grid-column: 13 / 16;
          grid-row: 1 / 4;
        }

        div:nth-of-type(8) {
          grid-column: 13 / 16;
          grid-row: 4 / 7;
        }

        div:nth-of-type(9) {
          grid-column: 16 / 19;
          grid-row: 1 / 4;
        }

        div:nth-of-type(10) {
          grid-column: 16 / 19;
          grid-row: 4 / 7;
        }

        div:nth-of-type(11) {
          grid-column: 19 / 25;
          grid-row: 1 / 5;
        }

        div:nth-of-type(12) {
          grid-column: 19 / 21;
          grid-row: 5 / 7;
        }

        div:nth-of-type(13) {
          grid-column: 21 / 23;
          grid-row: 5 / 7;
        }

        div:nth-of-type(14) {
          grid-column: 23 / 25;
          grid-row: 5 / 7;
        }

        .slideContent {
          @include flex(flex, column, center, center);
          position: relative;
          width: 100%;
          height: 100%;
          overflow: hidden;

          .articleText,
          .articleAuthor {
            width: 250px;
            color: white;
            text-align: center;
            line-height: 30px;
            z-index: 10;
            cursor: pointer;
          }

          .articleText {
            @include themeFont;
            font-size: 28px;
            word-break: keep-all;
          }

          .articleAuthor {
            margin-top: 15px;
            font-size: 15px;
          }

          .articleImage,
          .cover {
            position: absolute;
            width: 100%;
            height: 100%;
          }

          .articleImage {
            background-size: cover;
            background-position: center;
            transform: scale(1);
            transition: transform 400ms ease-in-out;
            z-index: 1;
          }

          &:hover .articleImage {
            transform: scale(1.1);
          }

          .cover {
            background-color: rgba(0, 0, 0, 0.2);
            transition: background-color 400ms ease-in-out;
            z-index: 5;
          }

          &:hover .cover {
            background-color: rgba(0, 0, 0, 0.4);
            cursor: pointer;
          }
        }
      }
    }

    .prevBtn,
    .nextBtn {
      position: absolute;
      top: 210px;
      width: 100px;
      height: 100px;
      border: none;
      border-radius: 100%;
      color: grey;
      background-color: white;
      opacity: 60%;
      font-size: 40px;
      font-weight: 100;
      cursor: pointer;

      &.hidden {
        display: none;
      }
    }

    .prevBtn {
      left: 100px;
    }

    .nextBtn {
      right: 100px;
    }
  }

  .contentsOrder {
    @include flex(flex, row, center);
    margin-top: 25px;

    .numBtn {
      margin-left: 15px;
      padding-bottom: 2px;
      color: rgb(204, 204, 204);
      font-size: 12px;
      cursor: pointer;

      &.active {
        color: black;
        border-bottom: 1px solid black;
        font-weight: bold;
      }
    }
  }
}
profile
성장형 프론트엔드 개발자

0개의 댓글