React에서 z-index 관리하기

낙타·2020년 8월 10일
8

웹 프론트엔드 개발자라면 Dropdown, Modal과 같은 z-index 값을 이용해야 하는 엘리먼트를 개발해야 하는 경우가 생기기 마련입니다. z-index는 엘리먼트의 position 속성이 static이 아닌 경우 효과가 나타나게 됩니다. 이 z-index 값의 숫자가 높을수록 더 위쪽에 보이게 됩니다.

그러다 보니 자칫 잘못하면 의도치 않게 다른 엘리먼트에 가려지게 되는 경우가 생깁니다. 이번 글에서는 React를 이용해서 개발 할 때 이런 상황을 조금이나마 피할 수 있도록 z-index를 관리해 주는 방법에 대해서 살펴보도록 하겠습니다.

보통의 z-index 관리

z-index의 값은 css의 속성이기 때문에 보통 고정 값으로 적용을 하게 됩니다. 예를 들어, 사용자의 이름을 누르면 해당 사용자의 프로필을 보여주는 레이어가 뜬다고 가정해 보겠습니다. 이때 프로필 레이어의 z-index는 30입니다.

그리고 어떤 모달하나를 가정해보겠습니다. 이 모달에는 사용자의 이름이 표시가 됩니다. 이 모달은 z-index 값이 50입니다. 그리고 이 이름을 누르면 위에 나왔던 사용자의 프로필을 보여주는 레이어가 뜨게 됩니다.

자 그럼, 이 상태에서 사용자의 이름을 누르면 어떻게 될까요? 당연히 아무것도 표시되지 않습니다. 모달의 z-index 값이 30 보다 큰 50이기 때문이죠. 그럼 개발자는 프로필 레이어의 z-index 값을 50보다 큰 값으로 수정해줘야 합니다. 넉넉하게 100정도로 올리면 될것 같습니다.

됐습니다. 이제 모달안에서도 사용자 이름을 눌러서 프로필을 확인할 수 있게 됐습니다.

자 그런데 서비스에 새 기능을 넣게 됐습니다. 사용자의 전화번호를 눌러서 문자를 보낼 수 있는 기능을 추가했습니다.

번호를 누르면 문자를 보낼 수 있는 모달을 하나 띄워주도록 한다고 가정합시다. 어떻게 될까요? 모달은 z-index 값을 공통으로 50을 사용한다고 한다면 아래와 같이 보이게 됩니다.

모달 안에서 프로필 레이어가 떠야 하기 때문에 50보다 큰 숫자인 100을 설정했기 때문입니다. 그렇다고 문자 보내기 기능 때문에 프로필 레이어의 z-index 값을 다시 50보다 작게 낮추면 다시 프로필 레이어가 초대 모달에서는 더 밑으로 내려가게 되겠죠.

다른 해결 방법은 모달마다 z-index 값을 따로 설정하면 되는데, 이런 모달들이 하나 둘씩 늘어나다보면 왜 특정 모달의 z-index값이 이런 값으로 설정됐는지 히스토리를 알지 못하면 이해하기 어렵게 됩니다.

이런 상황을 어떻게 하면 해결할 수 있을까요?

z-index를 관리하는 ZIndexer

방금과 같은 상황에서 처음 초대 모달은 z-index가 30 입니다. 그리고 프로필 레이어를 띄우게 될 경우 30보다 큰 숫자가 z-index 값으로 쓰이면 정상적으로 보이게 될것 같습니다. 마지막으로 문자 보내기 모달의 z-index 값은 프로필 레이어의 z-index 값보다 더 큰 숫자면 될것 같습니다.

이런식으로 마지막 z-index 값에서 필요할때 마다 z-index 값을 올려서 사용하면 어떨까요? 그러면 위에서 했던 것처럼 z-index 값을 이리 저리 수정하면서 관리하지 않아도 될것 같습니다.

자 그럼 이런 역할을 해줄 수 있는 ZIndexr라는 컴포넌트를 하나 만들어 보겠습니다.

const defaultZIndex = 30;

function ZIndexer({children}) {
  
  return children({ zIndex });
}

export default ZIndexer;

ZIndexer는 children에게 zIndex 값을 파라미터로 넘겨줘야하기 때문에 children은 함수형태가 돼야 합니다. children은 zIndex 값을 받을 수 있기 때문에 파라미터로 받은 zIndex 값을 이용해서 스타일을 적용해 주면 됩니다.

function Modal() {
  
  return (
    <ZIndexer>
      {({zIndex}) => (
        <div style={{zIndex}}>모달 내용</div>
      )}
    </ZIndexer>
  )
}

export default Modal;

지금의 ZIndex 컴포넌트는 무조건 30이라는 값을 넘겨주기 때문에 마지막 열었던 z-index 값을 기억하고 여기에 1을 더해서 넘겨주도록 수정합니다. 서비스 전체에서 사용된 z-index 값을 알아야 하기 때문에 React Context를 이용하도록 하겠습니다. Redux나 Mobx와 같은 다른 State Management 도구를 사용해서 관리해도 될것 같습니다.

우선 ZIndexContext를 만들어서 쓸수 있게 등록시켜줄 ZIndexProvider 컴포넌트를 만들어보겠습니다.

export const ZIndexContext = createContext({
  zIndexes: [],
  addZIndex: (_: number) => {},
  removeZIndex: (_: number) => {}
});

export function ZIndexProvider({children}) {
  const [zIndexes, setZIndexes] = useState([]);
  
  const addZIndex = (zIndex) => {
    setZIndexes(zIndexes => [...zIndexes, zIndex]);
  };
  
  const removeZIndex = (zIndex) => {
    setZIndexes(zIndexes => {
      const index = zIndexes.findIndex(usedZIndex => usedZIndex === zIndex);
      return zIndexes.filter((_, i) => i !== index);
    });
  }
  
  const value = { zIndexes, addZindex, removeZIndex };

  return (
    <ZIndexContext.Provider value={value}>
      {children}
    </ZIndexContext.Provider>
  )
}

자 이제 ZIndexer 컴포넌트에서 ZIndexContext를 이용해서 사용된 z-index 값을 알아낼 수 있습니다. useContext 훅을 이용해서 ZIndexContext에 저장된 값을 사용합니다. zIndexes가 비어있는 경우 이제 처음 zIndex가 필요하기 때문에 defaultIndex 값을 이용합니다. 그렇지 않은 경우 마지막 zIndex에서 1을 더한 값을 이용합니다.

const defaultZIndex = 30;

function ZIndexer({children}) {
  const { zIndexes, addZIndex, removeZIndex } = useContext(ZIndexContext);
  
  const hasLastIndex = zIndexes.length > 0;
  const nextZIndex = hasLastIndex ? zIndexes[zIndexes.length - 1] + 1 : defaultZIndex;
  
  return children({ zIndex: nextZIndex });
}

export default ZIndexer;

그런데 이렇게 하면 ZIndexContext에 사용한 zIndex 값이 저장되지 않기 때문에, ZIndexer에서 사용한 z-index 값을 ZIndexContext에 저장해주도록 합니다. 그러기 위해서 ZIndexer에서 사용할 zIndex 값을 저장하기 위해 useState 훅을 사용합니다. 그리고 useEffect 훅으로 처음 렌더링됐을때 한번만 zIndex 값을 계산하도록 해줍니다.

const defaultZIndex = 30;

function ZIndexer({children}) {
  const [zIndex, setZIndex] = useState(-1);
  const { zIndexes, addZIndex, removeZIndex } = useContext(ZIndexContext);
  
  useEffect(() => {
    const hasLastIndex = zIndexes.length > 0;
    const nextZIndex = hasLastIndex ? zIndexes[zIndexes.length - 1] + 1 : defaultZIndex;
    
    setZIndex(nextZIndex);
    addZIndex(nextZIndex);
    
    return () => {
      removeZIndex(nextZIndex);
    }
  }, []);
  
  return children({ zIndex });
}

export default ZIndexer;

자, 이렇게 ZIndexer 컴포넌트가 완성됐습니다. 이제 ZIndexer 컴포넌트를 사용해 볼까요? 우선은 ZIdexContext를 사용하기 때문에 서비스의 루트 컴포넌트에서 ZIndexProvider를 렌더링 해줍니다.

function App() {
  return (
    <ZIndexProvider>
      ...
    </ZIndexProvider>
  )
}

그리고 z-index가 필요한 곳에 ZIndexer 컴포넌트를 이용하면 됩니다.

function Modal() {
  return (
    <ZIndexer>
      {({zIndex}) => (
        <div style={{zIndex}}>모달 내용</div>
      )}
    </ZIndexer>
  )
}

function Profile({user}) {
  return (
    <ZIndexer>
      {({zIndex}) => (
        <div style={{zIndex}}>
          <div><img src="..."/></div>
          <div>{user.name}</div>
          <div>Tel. {user.tel}</div>
        </div>
      )}
    </ZIndexer>
  )
}

이렇게 z-index가 필요한 컴포넌트에 ZIndexer 컴포넌트로 한번 감싸서 항상 최신의 z-index 값을 받아서 사용하기만 하면 됩니다.

profile
함수형 프로그래밍에 관심이 많은, 프런트엔드 개발자입니다.

0개의 댓글