React로 "...더보기" 버튼 만들기 (+반응형)

아리·2021년 3월 2일
7

부족한 부분이 많으니, 좀 더 좋은 방법이 있다면 알려주시면 감사하겠습니다!

배경

"프론트엔드 신입으로 회사에 입사해 모바일 대응만 하면 되는 페이지를 만들고 있던 중, 발목을 잡은 디자인이 있었는데..."

디자인 팀장님께서는 세줄까지 나온 뒤 더보기 버튼을 클릭하면 밑으로 내용이 나올 수 있도록 요청 하셨고, 처음엔 별거 아니라 생각했다.

방법

1. 글자 수 제한

먼저 처음 생각한 방법은
자바스크립트로 글자 수 제한을 둬서 그 글자 수 이상이 넘어가면 잘리도록 하는 방법이였다.
( 인스타그램 등 많은 곳이 이방식으로 처리하는 것 같아 보였다. )

const Box = () => {
  const [limit, setLimit] = useState(50); 
  const toggleEllipsis = (str, limit) => {
    return {
    	string: str.slice(0, limit),
      	isShowMore: str.length > limit
    }
  };

  const onClickMore = (str) => () => {
    setLimit(str.length);
  };
  
  return (
    <Wrapper>
      <Description>
        {toggleEllipsis(text, limit).string}
        {toggleEllipsis(text, limit).isShowMore && <MoreButton onClick={onClickMore(text)}>...더보기</MoreButton>}
      </Description>
    </Wrapper>
  )
}

문제
적당한 모바일 화면에서는 잘 되는 것 같지만, 반응형 대응이 잘 안되어 마음에 썩 들지 않았다.
화면이 큰 모바일의 경우 텍스트들이 한줄이 되는데도 불구하고 더보기 버튼이 생성되게 되는데, 이런 경우에는 더보기 버튼은 없어지고 내용이 다 보여야 한다고 생각했다.
사용할만한 상황: 텍스트 줄 수에 크게 상관이 없고, 적당히 텍스트의 글이 보일 만큼 보이고 더보기로 가려놓길 바란다면 사용하기 좋을 것 같다.
근데 이게 디자인과 가장 비슷하게 나온다..(예고)

2. css로 처리 & 버튼 띄우기

두번째 방법은,
여러줄을 말줄임 처리 할 수 있는 css를 이용해 세줄 이상이 되면 텍스트를 자르고 버튼을 오른쪽 맨 끝에 띄워 버튼에 background linear-gradient 를 주는 트릭을 이용하는 방법이였다.

아래는 완성 모습이다.

const Box = () => {
  const contentRef = useRef(null);
  const onClick = (e) => {
    contentRef.current.classList.add("show");
    e.currentTarget.classList.add("hide");
  };
  return (
    <Wrap>
      <Ellipsis ref={contentRef}>{data}</Ellipsis>
      <Button>...더보기</Button>
    </Wrap>
  )
}

const Ellipsis = styled.div`
  position: relative;
  display: -webkit-box;
  max-height: 6rem;
  line-height: 2rem;
  overflow: hidden;
  -webkit-line-clamp: 3;
  &.show {
      display: block;
      max-height: none;
      overflow: auto;
      -webkit-line-clamp: unset;
    }
`;

const Button = styled.button`
  position: absolute;
  bottom: 0;
  right: 0;
  max-height: 2rem;
  line-height: 2rem;
  padding-left: 20px;
  background: rgb(2, 0, 36);
  background: linear-gradient(
    90deg,
    rgba(2, 0, 36, 1) 0%,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 1) 18%
  );
  &.hide {
    display: none;
  }
`;

문제
일단 디자인과 다르다. (이게 제일 큰 문제)
백그라운드 때문에 자연스러운듯 자연스럽지 않다.😳

3. 협의 후 더보기 버튼 세줄 밑으로 떨어지도록

마지막으로는 텍스트 세 줄을 꽉 채워 css로 자른 뒤,
더보기 버튼을 텍스트 밑으로 내려 사용하기

이때, 가장 신경쓰였던 resize 될 때의 상황인데,
계속해서 resize되는 모습을 지켜보고 있어야 scrollHeight 값이 height 값보다 커지는지 아닌지를 알 수 있다. (텍스트 내용이 세줄이 되지 않을 경우, 더보기 버튼 안보이게 하기 위해)

구글 검색하다가 찾아낸 ResizeObserver 와 이를 활용해 만든 useResizeObserver 커스텀훅.
검색으로 찾아낸 커스텀훅을 이용해 세 줄이 되지 않거나 텍스트가 짧을 경우 더보기 버튼이 보이지 않도록, 반대라면 세 줄 아래로 더보기 버튼이 보이도록 구현했다.

아래는 완성된 모습이다.

// useResizeObserver.js
import { useEffect, useRef } from "react";
import ResizeObserver from "resize-observer-polyfill";

const useResizeObserver = ({ callback, element }) => {
  const current = element && element.current;
  const observer = useRef(null);

  useEffect(() => {
    if (observer && observer.current && current) {
      observer.current.unobserve(current);
    }
    const observe = () => {
      if (element && element.current && observer.current) {
        observer.current.observe(element.current);
      }
    };
    const resizeObserverOrPolyfill = ResizeObserver;
    observer.current = new resizeObserverOrPolyfill(callback);
    observe();

    return () => {
      if (observer && observer.current && element && element.current) {
        observer.current.unobserve(element.current);
      }
    };
  }, [callback, current, element]);
};

export default useResizeObserver;
// main
const App = () => {
  const contentRef = useRef(null);
  const [isShowReadMore, setIsShowReadMore] = useState(false);
  const observeCallback = (entries) => {
    for (let entry of entries) {
      if (entry.target.scrollHeight > entry.contentRect.height) {
        setIsShowReadMore(true);
      } else {
        setIsShowReadMore(false);
      }
    }
  };
  useResizeObserver({ callback: observeCallback, element: contentRef });
  const onClick = (e) => {
    contentRef.current.classList.add("show");
    setIsShowReadMore(false);
  };
return (
    <Wrap>
      <Ellipsis ref={contentRef}>{data}</Ellipsis>
      {isShowReadMore && <Button onClick={onClick}>...더보기</Button>}
    </Wrap>
  );
}

const Wrap = styled.div``;

const Ellipsis = styled.div`
  position: relative;
  display: -webkit-box;
  max-height: 6rem;
  line-height: 2rem;
  overflow: hidden;
  -webkit-line-clamp: 3;
  &.show {
    display: block;
    max-height: none;
    overflow: auto;
    -webkit-line-clamp: unset;
  }
`;

const Button = styled.button`
  max-height: 2rem;
  line-height: 2rem;
  &.hide {
    display: none;
  }
`;

디자인 팀장님께 말씀드리고 두번째 혹은 세번째 방법으로 처리 하기로 했는데 세번째가 나을 것 같다고 하셔서 최종적으로는 마지막 방법으로 처리 했다.

정리

나중에 찾아보니 npm에 등록된 패키지들도 꽤 있는것 같아 저장소에 가서 코드를 봤는데, 텍스트들의 각 줄들을 span 태그로 감싸서 텍스트들을 자르고 붙이는 식으로 처리 하는 것 같았다.😨
나중에 시간이 난다면 나만의 패키지 처럼 만들어 봐도 좋을 것 같고, 급한 경우엔 생산성을 위해 잘 만들어진 패키지를 이용하는 방법이 좋을 것 같다. 🙃

지금 생각해보면 많은 곳들이 글자 수 제한으로 처리하는 이유가 있는 듯 하다.. 🤔

References

React Hooks — useObserve (use ResizeObserver Custom Hook)
ResizeObserver - Web APIs | MDN

profile
프론트엔드 🌱

0개의 댓글