[React] 투두리스트 수정해보기 (2)

In9_9yu·2021년 11월 17일
1

TodoList

목록 보기
2/8
post-thumbnail

0.오늘은 뭐했니

🌀 portal을 이용한 Modal 구현 및 삽질

1. Modal을 어떻게 띄울 것인가?

지난번의 결과를 보자. 뭔가 이상하다 🤔
결과

생각하지 않고 디자인을 따라하다보니, 할 일 입력창이 사라지게 된 모습을 볼 수 있었다. 그래서 Add new 버튼을 누르면 모달을 띄우고, 모달 내부에서 입력을 받을 수 있도록 만들어야겠다고 계획을 급히 수정한 것은 비밀.

현재 컴포넌트 구조는 대략적으로 다음과 같다.

컴포넌트 구조

일단 모달을 띄우는 것이 우선이니 어떻게 하면 모달을 띄울 수 있을지 생각해보았다.

  • 모달의 visibility를 결정해줄 modal state 필요
  • 결국 new buttonn이 modal state를 변경해 줄 것이므로 new button과 modal은 같은 레벨에 있어야겠네 라고 생각
  • 근데 Modal 컴포넌트가 Header안에 있는 것은 아무리 생각해도 말이 안됨 ( Modal컴포넌트는 App Container와 동일한 레벨에서 있어야 하는 생각이 자꾸 들었음 )
  • 간단한 모달하나 만드는데 라이브러리는 사용하기 싫음 (????)

다행히도 react modal 이라는 키워드로 검색을 하다보니 공식문서에 떡하니 Portal이라는 개념이 있었다.무려 예제로 모달을 만들어주는 친절한 공식문서라니...

2. 🌀 Portal이 뭔데

Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공합니다.

응...그렇구나...?
이렇게만 보니 정말 뜬 구름 같은 느낌이라 예제를 더 읽어보았다.

CRA로 리액트 프로젝트를 시작한다면 다음과 같은 파일들이 있다.

public/index.html

<!DOCTYPE html>
<html lang="en">
  ...
  <body>
    <div id="root"></div> <- 요기 있네
  </body>
</html>

src/index.js

ReactDOM.render(<App />, document.getElementById("root"));

우리가 작성하는 모든 컴포넌트들은 결국 root라는 DOM에 붙게된다.이 점을 유의 해서 위의 설명을 다시 보면 아래와 같은 구조도 가능하다는 것을 알 수 있다.

<!DOCTYPE html>
<html lang="en">
  ...
  <body>
    <div id="root"></div>
    <div id="modal"></div> <-- 내가 작성한 아이들을 요기다 렌더링 해줘
  </body>
</html>

실제 코드가 그렇게 어렵지는 않아서 다음과 같이 작성해주었다.

src/portal.js

import reactDom from "react-dom";

export const ModalPortal = ({ children }) => {
  const $modal = document.getElementById("modal");
  return reactDom.createPortal(children, $modal);
};

src/App.js

if (isMobile) {
    return (
      <>
        <AppContainer>
          <ModalPortal>
            {modalState && <TodoMobileAddPostModal toggleModal={toggleModal} />}
          </ModalPortal>
      		   ...
        </AppContainer>
      </>
    );
  }

그리고 modalState를 App.js에서 관리하지만, Add new 버튼에서 값을 바꿀 수 있도록 하기 위해 다음과 같은 함수를 작성하여 props로 전달했다.

  const toggleModal = () => setModalState((prev) => !prev);

결과
결과

2. 근데 모달은 어떻게 닫을건데?

이제 모달을 열었으니, 다시 모달을 닫을 차례.
모달을 닫는 경우는 두 가지 케이스가 있다.

  • 글을 다 써서 엔터 혹은 확인 키를 눌렀을 때
  • 글쓰기를 취소하고 싶어서 나갈 때

첫 번째 경우야 Enter 키를 받았을 때 toggleModal 함수를 실행시키면 되는 것이라 금방 해결했다.

문제는 두 번째 경우인데, 가장 간단하게는 버튼 하나 만들어서 toggleModal을 넘겨주면 된다.

하지만 다른 부분을 눌렀을 때 모달을 닫을 수 있다면 얼마나 아름다울까 (?)

이 방법은 미니펫피에 프론트로 참여했을 때 킹갓 프론트 팀장 분께서 작성하신 코드가 있어서 참고하기로 했다.

useClickOutside.ts

import React, { useEffect } from 'react';

export const useClickOutside = (targetRef: React.RefObject<HTMLElement>, callback: () => void) => {
  useEffect(() => {
    const outSideCallback = (e: MouseEvent) => {
      const target = e.target as HTMLElement;

      if (targetRef && !targetRef.current.contains(target)) {
        callback();
      }
    };

    document.addEventListener('mousedown', outSideCallback);
    return () => {
      document.removeEventListener('mousedown', outSideCallback);
    };
  }, [targetRef, callback]);
};

작성하면서 잠시 헤맸던 점

  • Node.contains라는게 있나...?

  • 그냥 targetRef.current !== target 하면 안되나?

    • 안된다. 단적인 예는 다음과 같다.

    예시

    이렇게 되면 저 input을 클릭하게 되면 targetRef !== input이기 때문에 모달이 닫히게 된다.

결과
결과

3. 궁금한 점

그냥 포탈을 사용할때는 별로 의식하지 못했는데, 글을 작성하다보니 의문이 드는 부분이 있었다.

분명 포탈은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링 한다고 되어있다.
modal의 position은 absolute로 설정이 되어있는데, 보통 absoulte는 position이 static이 아닌 블럭을 기준으로 자리를 잡는다고 알고 있는데...
그럼 누구를 기준으로 하는거지?

<!DOCTYPE html>
<html lang="en">
  ...
  <body> <-- 얘도 아닌데...
    <div id="root"></div> <-- 얘도 아니고
    <div id="modal"></div> 
  </body>
</html>

MDN 문서를 보니 다음과 같이 적혀있었다.

absolute
단, 조상 중 위치 지정 요소가 없다면 초기 컨테이닝 블록을 기준으로 삼습니다.

컨테이닝 블록
참고: 루트 요소()의 컨테이닝 블록은 초기 컨테이닝 블록이라고 불리는 사각형입니다. 초기 컨테이닝 블록은 뷰포트 또는 (페이지로 나뉘는 매체에선) 페이지 영역의 크기와 같습니다.

아하...! HTML...!

오늘의 삽질 끝

profile
FE 임니다

0개의 댓글

관련 채용 정보