본문 바로가기
프론트엔드

(공통 컴포넌트) z-index 관리하기

by 흥부와놀자 2024. 3. 9.

오버레이 공통 컴포넌트인 Modal, PopOver를 구현하기 위해선 각각의 z-index를 관리할 수 있어야 한다. 

 

z-index에 대해 알아보자.

 

z-index란? 

https://www.thegeekyway.com/z-index-in-css/

위 사진과 같이 각 html 요소들의 z축을 정의하는 속성이다. 

z축이 높을수록 앞으로 튀어 나온다고 보면 된다.

 

z-index 쌓임 맥락(stacking context)

하지만 무조건 z-index가 크다고 해당 요소의 z축이 높게 잡히는게 아니다.

요소의 posiiton, 부모의 z-index등이 어떻냐에 따라 실제 z축이 잡히게 된다. 

포토샵 레이어를 생각하면 된다. 위 사진에서 Cat의 Moustache의 z축이 아무리 높다해도, Dog가 Cat보다 위에 있다면 강아지 사진을 역전할 수 없다. 

이렇게 z-index는 그룹화되어 사용되고, 여러 환경에서 z축이 쌓이는걸 쌓임맥락이라고 한다.

 

이렇게 쌓임 맥락이 생성되려면 몇가지 조건이 필요하다.

1. posiiton + z-index 속성

요소의 position이 relative, absolute이고 z-index가 auto가 아닐때 쌓임 맥락이 생성된다. 

<div
  style={{ zIndex: 100, backgroundColor: 'blue', height: '100px', width: '100px' }}>
  blue
</div>
<div
  style={{
    position: 'absolute',
    zIndex: 10,
    top: '80px',
    backgroundColor: 'green',
    height: '80px',
    width: '80px',
  }}>
  green
</div>

 

 

 

green의 경우 쌓임맥락이 생성된 상태이고, blue는 생성이 안된 상태이기 때문에 blue의 z축이 낮게 설정된다.

 

blue가 역전하기 위해서는 같은 맥락을 만들어주던가 아래와 같이 맥락을 가진 부모를 만들어 줄수도 있다.

<div style={{ position: 'absolute', zIndex: 99 }}>
  <div
    style={{
      position: 'absolute',
      zIndex: 100,
      backgroundColor: 'blue',
      height: '100px',
      width: '100px',
    }}>
    blue
  </div>
</div>
<div
  style={{
    position: 'absolute',
    zIndex: 10,
    top: '80px',
    backgroundColor: 'green',
    height: '80px',
    width: '80px',
  }}>
  green
</div>

 

또한 같은 맥락의 같은 z-index인 경우 아래와 같이 요소가 뒤에 선언될수록 z축이 높게 잡힌다. 

 

<div
  style={{
    position: 'absolute',
    zIndex: 10,
    backgroundColor: 'blue',
    height: '100px',
    width: '100px',
  }}>
  blue
</div>
<div
  style={{
    position: 'absolute',
    zIndex: 10,
    top: '80px',
    backgroundColor: 'green',
    height: '80px',
    width: '80px',
  }}>
  green
</div>

같은 맥락이지만 선언순서 다름

이렇게 z-index는 그때그때 환경에 따라 다르게 나오는데, 개발하는 입장에선 이런부분을 하나하나 신경쓰기가 피곤하다.

 

z-index를 사용하는 모달이나 팝업 같은 오버레이 컴포넌트들의 z-index를 같은 쌓임 맥락에 둘수 없을까?

 

ReactPortal

먼저 React에서 제공하는 Portal 기능을 사용해보자.

React는 구조상 상위 root div에서 시작하는데, 나는 root div의 형제위치에 overlay div라는 공통의 부모를 두고 거기서 모달이나 팝오버같은 오버레이 컴포넌트들을 같은 쌓임맥락으로 관리할것이다.

 

ReactDOM.createPortal을 사용하면 해당 요소를 root밖의 특정 돔 요소아래에 렌더링 시킬 수 있다.  

 

ReactDOM.createPortal(
   ... popover 코드,
  // overlay 끼리 공정한 z-index 비교를 위해 리액트 포탈을 이용해 overlay div 에 추가
  document.getElementById('overlay')
)

해당 overlay 컴포넌트를 Open해줄때 위와 같이 Portal로 렌더링 시켜준다.

그리고 오버레이 컴포넌트를 만들떄 최외곽 div의 position과 z-indexer값을 통일 하면

 

결국 어떤 overlay컴포넌트든 나중에 Open되는 컴포넌트가 더 높은 z축을 가지게 된다.

같은 쌓임맥락이기에 동일한 z-index라면 요소가 뒤에 있을수록 z축이 커지기 때문이다. 

그리고 오버레이 컴포넌트 안에 오버레이 컴포넌트가 있다고 해도 부모 자식 상관없이 overlay div에 flat하게 들어가기 때문에 중첩될때 상관 관계를 신경쓰지 않아도 된다.

 

물론, React portal만으로도 충분하겠지만 혹시 컴포넌트 별로 최외곽 요소의 z-index가 달라질 경우에 대비하여, 

같은 overlaydiv에 추가되는 오버레이 컴포넌트들에게 추가되는 순서에 따라 오름차순으로 z-index를 부여해주면 조금 더 확실하게 관리 할 수 있을 것이다. (사실 이런경우는 드물긴 하지만 z-index를 통합해서 관리할수 있단 점에서 나중에 필요할 날이 있지 않을까 한다)

 

ZIndexer

구현은 간단하다. React의 Context Api를 활용하여 ZIndexer로 감싸진 오버레이 컴포넌트들이 렌더링 될때 (Open될때) Context에서 전역으로 관리되는 현재 z-index값에 1을 더하여 해당 오버레이 컴포넌트 최외곽 요소의 스타일로 내려준다. 

 

ZIndexContext

const ZIndexContext = React.createContext<Zindexprops>({
  zIndexes: [],
  addZIndex: (_: number) => {},
  removeZIndex: (_: number) => {}
});

 

ZIndexProvider

export const ZIndexProvider: React.FC = props => {
  const [zIndexes, setZIndexes] = useState([]);

  // zindex 추가
  const addZIndex = (zIndex: number) => {
    setZIndexes(zIndexes => [...zIndexes, zIndex]);
  };
  // zindex배열에서 해당 zindex의 인덱스 찾아서 해당 인덱스 빼고 배열 재구성
  const removeZIndex = (zIndex: number) => {
    setZIndexes(zIndexes => {
      const index = zIndexes.findIndex(usedZIndex => usedZIndex === zIndex);
      return zIndexes.filter((item, i) => i !== index);
    });
  };

  return (
    <ZIndexContext.Provider value={{ zIndexes, addZIndex, removeZIndex }}>{props.children}</ZIndexContext.Provider>

이렇게 ContextApi를 사용하여 zindex를 추가하고 삭제하는 메소드와 zindex상태를 관리해준다.

 

ZIndexer

export const ZIndexer: React.FC<ZIndexerProps> = props => {
  const [zIndex, setZIndex] = useState(-1);

  const { zIndexes, addZIndex, removeZIndex } = useContext(ZIndexContext);
  useEffect(() => {
    // zindexes 배열이 없으면 기본값, 그렇지 않으면 1씩 더해진 값이 추가됨
    const hasLastIndex = zIndexes.length > 0;
    const nextZIndex = hasLastIndex ? zIndexes[zIndexes.length - 1] + 1 : defaultZIndex;
    // 추가된 값을 해당 컴포넌트에 상태 세팅, 전역Context에 세팅 함
    addZIndex(nextZIndex);
    setZIndex(nextZIndex);
    console.log('nextxzindex', nextZIndex);
    return () => {
      removeZIndex(nextZIndex);
    };
  }, []);
  return (
    <>
      {/*해당 컴포넌트의 자식에게 zindex 붙여줌*/}
      {React.Children.map(props.children, (child, index) =>
        React.cloneElement(child as React.DetailedReactHTMLElement<any, HTMLElement>, {
          style: { zIndex: zIndex }
        })
      )}
    </>
  );
};

그 후 ZIndexer를 감싼 오버레이컴포넌트가 마운트될떄 마다  ContextApi의 zindex값을 가져와서 자식에 세팅해주고 zindex를 추가하는 함수를 호출해준다. 

zindex값을 세팅해줄때는 cloneElement를 사용하여 해당 자식에 스타일만 추가해주도록 했다. 

 

return props.open ? (
  ReactDOM.createPortal(
    <ZIndexer>
      <div className="dimmed on">
        <div className={modalClassName + props.className} ref={modalRef}>
          <div style={{ maxHeight: props.maxHeight }}>{props.children}</div>
        </div>
      </div>
    </ZIndexer>,
    document.getElementById('overlay')
  )
) : (
  <></>
);

위 코드는 오버레이 컴포넌트들에 공통으로 들어가는 포맷이고, 해당 컴포넌트가 open됬을때, createPortal로 overlay div에 렌더링 시켜주고, 그안에서 ZIndexer를 통해 overlay div자식들의 z-indexer를 확실하게 명시해준다.