티스토리 뷰

개발 부스러기는 완결된 형식의 글이 아닌, 다양한 시행착오를 기록하는 글입니다.

1. 요구사항

 

  • 부모 요소(투명)가 있고 그 아래에 자식 요소가 왼쪽(초록), 오른쪽(핑크) 끝에 배치되어야 함 (모두 flexbox 적용)
  • 왼쪽 자식 요소의 아래에 헤딩 또는 span 요소로 감싼 긴 텍스트(파랑)가 들어가는데, 공백으로 인한 줄 바꿈이 일어나지 않는 상태
  • 따라서 텍스트 오버플로우가 발생할 수 있음. 만약 오버플로우가 발생했다면, 오른쪽(핑크)의 영역을 침범했는지 확인
  • 침범한 경우 오버플로우로 인해 잘린 텍스트를 표시하기 위해 다음 사항을 적용
    • 텍스트 요소에 마우스를 올렸을 때(hover) 왼쪽으로 스크롤되는 애니메이션 실행

2. 오버플로우 감지하기

1. 상태값 및 DOM 요소 참조값 선언

  • 오버플로우가 발생했는지를 나타내는 상태값 'isOverflow'
  • 텍스트가 들어간 헤더 요소를 참조하는 값 'leftTextRef'
  • 오른쪽 div 요소를 참조하는 값 'rightDivRef'
const [isOverflow, setIsOverflow] = useState(false);
const leftTextRef = useRef<HTMLHeadingElement>(null);
const rightDivRef = useRef<HTMLDivElement>(null);

2. 오버플로우 감지하기

 useEffect 훅을 사용하여 컴포넌트가 렌더링 될 때 오버플로우를 감지하여 'isOverflow' 상태값을 변경해 줌.

useEffect(() => {
  const checkOverlapped = () => {
    if (leftTextRef.current && rightDivRef.current) {
      const textRect = leftTextRef.current.getBoundingClientRect();
      const divRect = rightDivRef.current.getBoundingClientRect();

      setIsOverflow(textRect.right > divRect.left);
    }
  };

  checkOverlapped();
  window.addEventListener("resize", checkOverlapped);
  return () => window.removeEventListener("resize", checkOverlapped);
}, [text]);

 이 때 참조한 각 요소로부터 'getBoundingClientRect'를 호출하여 뷰포트(브라우저 화면)로부터 요소의 상대적인 위치 정보를 가져옴 (픽셀 단위).

  • left 또는 x: 뷰포트의 왼쪽 경계(0, 0)에서 해당 요소의 왼쪽 경계까지의 수평 거리.
  • right: 뷰포트의 왼쪽 경계(0, 0)에서 해당 요소의 오른쪽 경계까지의 수평 거리.
  • top 또는 y: 뷰포트의 위쪽 경계(0, 0)에서 해당 요소의 위쪽 경계까지의 수직 거리.
  • bottom: 뷰포트의 위쪽 경계(0, 0)에서 해당 요소의 아래쪽 경계까지의 수직 거리.

getBoundingClientRect

const checkOverlapped = () => {
  if (leftTextRef.current && rightDivRef.current) {
    const textRect = leftTextRef.current.getBoundingClientRect();
    const divRect = rightDivRef.current.getBoundingClientRect();

    setIsOverflow(textRect.right > divRect.left);
  }
};

 크롬 브라우저에서 데브툴을 사용해 각 요소의 경계를 살펴보면, 다음과 같이 왼쪽 텍스트 요소의 right 값이 오른쪽 div 요소의 left 값을 넘어섰으므로 오버플로우가 발생했다는 것을 알 수 있음.

overflow 감지

 텍스트의 길이가 짧아서 오른쪽 div 요소를 침범하지 않는 경우는 다음과 같음.

overflow 없음

 

 마지막으로 윈도 크기가 변함에 따라 checkOverlapped 함수를 다시 호출하여 오버플로우를 감지하도록 함.

checkOverlapped();
window.addEventListener("resize", checkOverlapped);
return () => window.removeEventListener("resize", checkOverlapped);

3. 애니메이션 구현 및 적용하기

 텍스트 요소에 마우스가 올라갔을 때 실행될 애니메이션을 css 파일에 정의.

@layer utilities {
  @keyframes scroll-text {
    0% {
      transform: translateX(0%);
    }
    90% {
      transform: translateX(-100%);
    }
    95%,
    100% {
      transform: translateX(0%);
    }
  }

  .hover-scroll-text:hover {
    animation: scroll-text 7s linear infinite;
  }
}

 논리 연산자를 사용해  'isOverflow' 값이 true인 경우에만 클래스 이름이 적용되도록 함.

<h5
  className={`whitespace-nowrap ${isOverflow && "hover-scroll-text"}`}
  ref={leftTextRef}
>
  {text}
</h5>

결과물


4. 전체 코드

app/page.tsx

"use client";

import { useEffect, useRef, useState } from "react";

export default function Home() {
  const [text, setText] = useState("왼쪽 컴포넌트");
  const [isOverflow, setIsOverflow] = useState(false);
  const leftTextRef = useRef<HTMLHeadingElement>(null);
  const rightDivRef = useRef<HTMLDivElement>(null);

  const toggleText = () => {
    setText(
      text === "왼쪽 컴포넌트"
        ? "왼쪽 컴포넌트의 내용이 여기에 들어갑니다. 이 내용이 길어지면 오른쪽 컴포넌트를 침범할 수 있습니다. 애니메이션을 추가하여 스크롤링 효과를 줄 수 있습니다."
        : "왼쪽 컴포넌트"
    );
  };

  useEffect(() => {
    const checkOverlapped = () => {
      if (leftTextRef.current && rightDivRef.current) {
        const textRect = leftTextRef.current.getBoundingClientRect();
        const divRect = rightDivRef.current.getBoundingClientRect();

        setIsOverflow(textRect.right > divRect.left);
      }
    };

    checkOverlapped();
    window.addEventListener("resize", checkOverlapped);
    return () => window.removeEventListener("resize", checkOverlapped);
  }, [text]);

  return (
    <div className="flex flex-col gap-2 justify-center items-center min-h-screen bg-gray-50">
      {/* 왼쪽 컴포넌트와 오른쪽 컴포넌트를 포함하는 컨테이너 */}
      <div className="flex justify-between w-[400px] bg-gray-100 p-4">
        {/* 왼쪽 컴포넌트 */}
        <div className="flex flex-grow items-center p-4 bg-blue-200 overflow-hidden">
          <h5
            className={`whitespace-nowrap ${isOverflow && "hover-scroll-text"}`}
            ref={leftTextRef}
          >
            {text}
          </h5>
        </div>

        {/* 오른쪽 컴포넌트 */}
        <div
          className="flex min-w-[100px] p-4 bg-red-200 z-10"
          ref={rightDivRef}
        >
          오른쪽 컴포넌트
        </div>
      </div>
      <button className="p-2 bg-blue-500 text-white" onClick={toggleText}>
        텍스트 변경
      </button>
    </div>
  );
}

app/global.css

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer utilities {
  @keyframes scroll-text {
    0% {
      transform: translateX(0%);
    }
    90% {
      transform: translateX(-100%);
    }
    95%,
    100% {
      transform: translateX(0%);
    }
  }

  .hover-scroll-text:hover {
    animation: scroll-text 7s linear infinite;
  }
}

5. Advanced

 약간의 트릭을 첨가하여 부모 컨테이너에서 사용가능한 너비를 초과하지 않고도 왼쪽 컴포넌트가 항상 오른쪽 컴포넌트 아래에 있는 것처럼 보이게 해 보자.

"use client";

import { useEffect, useRef, useState } from "react";

export default function Home() {
  const [text, setText] = useState("왼쪽 컴포넌트");
  const [isOverflow, setIsOverflow] = useState(false);
  const leftTextRef = useRef<HTMLHeadingElement>(null);
  const rightDivRef = useRef<HTMLDivElement>(null);

  const toggleText = () => {
    setText(
      text === "왼쪽 컴포넌트"
        ? "왼쪽 컴포넌트의 내용이 여기에 들어갑니다. 이 내용이 길어지면 오른쪽 컴포넌트를 침범할 수 있습니다. 애니메이션을 추가하여 스크롤링 효과를 줄 수 있습니다."
        : "왼쪽 컴포넌트"
    );
  };

  useEffect(() => {
    const checkOverlapped = () => {
      if (leftTextRef.current && rightDivRef.current) {
        const textRect = leftTextRef.current.getBoundingClientRect();
        const divRect = rightDivRef.current.getBoundingClientRect();

        setIsOverflow(textRect.right > divRect.left);
      }
    };

    checkOverlapped();
    window.addEventListener("resize", checkOverlapped);
    return () => window.removeEventListener("resize", checkOverlapped);
  }, [text]);

  return (
    <div className="flex flex-col gap-2 justify-center items-center min-h-screen bg-gray-50">
      {/* 왼쪽 컴포넌트와 오른쪽 컴포넌트를 포함하는 컨테이너 */}
      <div className="w-[400px] bg-gray-100 p-4">
        <div className="flex justify-between">
          {/* 왼쪽 컴포넌트 */}
          {/* 
            max-w-0: 컨테이너의 최대 너비를 0으로 설정하여 오른쪽 컴포넌트와의 너비의 합이 상위 컨테이너에서 사용 가능한 너비를 초과하지 않도록 함
            overflow-visible: 컨테이너를 벗어나는 요소를 숨기지 않고 보여줌
          */}
          <div className="flex max-w-0 p-4 overflow-visible">
            {/* 
              max-w-[240px]: 왼쪽 컴포넌트의 최대 너비를 240px로 설정하여 텍스트가 길어지면 오른쪽 컴포넌트를 침범하지 않도록 함
              >> 부모 컨테이너의 최대 너비가 0이지만 자식 요소의 최대 너비가 240px으로 설정되어 있어 텍스트가 정상적으로 표시됨
            */}
            <div className="flex items-center max-w-[240px] gap-2">
              <div className="flex flex-shrink-0 w-12 h-12 bg-blue-200 rounded-2xl items-center justify-center">
                뱃지
              </div>
              {/* 
                whitespace-nowrap: 텍스트가 너비를 벗어나도 줄바꿈을 하지 않고 한 줄로 표시함
                overflow-hidden: 텍스트가 너비를 벗어나는 부분을 숨김
              */}
              <div className="flex whitespace-nowrap overflow-hidden">
                {/* 
                  hover-scroll-text: 텍스트가 오른쪽 컴포넌트를 침범할 때 마우스를 올리면 텍스트를 스크롤링하는 애니메이션을 추가함
                */}
                <h5
                  className={`${isOverflow && "hover-scroll-text"}`}
                  ref={leftTextRef}
                >
                  {text}
                </h5>
              </div>
            </div>
          </div>

          {/* 오른쪽 컴포넌트 */}
          {/* 
            z-10: 오른쪽 컴포넌트를 왼쪽 컴포넌트 위에 표시하기 위해 z-index를 설정함
          */}
          <div
            className="flex items-center p-4 bg-red-200 z-10"
            ref={rightDivRef}
          >
            오른쪽 컴포넌트
          </div>
        </div>
      </div>
      <button className="p-2 bg-blue-500 text-white" onClick={toggleText}>
        텍스트 변경
      </button>
    </div>
  );
}
  • 기존에는 왼쪽 컴포넌트가 자식 요소의 너비를 모두 더한 만큼의 너비를 가지게 되고, 이로 인해 자식 요소의 너비가 너무 긴 경우 가용할 수 있는 너비를 초과하게 되어 '오른쪽 컴포넌트'가 부모 영역 밖으로 밀려날 수 있음

  • 기본적으로 flex-wrap 속성은 nowrap 값으로 설정되기 때문에 max-width가 0이 된 요소의 자식 요소들은 오버플로우 되어 표시됨
  • 이렇게 되면 오른쪽 컴포넌트의 너비가 일정하게 유지된다는 가정하에 왼쪽 컴포넌트의 자식 컴포넌트의 너비만 오른쪽 컴포넌트의 오른쪽 경계를 넘어가지 않도록 조정을 하면, 왼쪽 컴포넌트의 자식 요소가 항상 오른쪽 컴포넌트의 아래에 배치되는 것과 같은 효과를 낼 수 있음


6. 참고

 

Element.getBoundingClientRect() - Web API | MDN

Element.getBoundingClientRect() 메서드는 엘리먼트의 크기와 뷰포트에 상대적인 위치 정보를 제공하는 DOMRect 객체를 반환합니다.

developer.mozilla.org

 

최근에 올라온 글
최근에 달린 댓글
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Total
Today
Yesterday
글 보관함