DRAKE

RESUME

React에서 overlay(modal,bottomSheet,toast)들을 만들고 관리하기

2024/01/08

8 min read

DEVELOPMENT

REACT

DESIGNSYSTEM

thumbnail

Overlay 컴포넌트들을 어떻게 만들지

modal같은 경우에는 React createPortal을 이용해서 만들고 관리는 해보았지만 바텀시트와 토스트 알림같은 경우에는 해보지 않아서 어떻게 개발을 하는지 궁금했습니다.

모달처럼 사용하면 되지 않을까라는 생각을 해봤었습니다. 기존 모달은 다음과 같이 사용을 했습니다

1

function Page(){

2

const [modalOpen,setModalOpen] = useState(false)

3

const Portal = usePortal()

4

...

5

6

return(

7

...

8

{modalOpen &&

9

<Portal>

10

<Modal>...</Modal>

11

</Portal>

12

}

13

)

14

}

모달에 대한 상태값이 필요했고 토스트와 바텀시트를 생성하기에는 관리하는데 어려워보였습니다.

그러던 중에 토스의 오픈소스 라이브러리인 slash를 보게되었고 그 안에서 useOverlay라는 hook을 보게되었습니다. 제가 필요했던 기능을 구현할 수 있겠다는 생각이 들었습니다. 코드들을 뜯어보며 개발을 해보고 어떻게 개발을 했는지 공유를 하고 싶어 글을 작성하기로 하였습니다.

slash에 있는 코드와 완전히 동일하지는 않습니다

Overlay 관리 방법

Provider

먼저 Portal을 이용하여 할 수도 있지만 useOverlay에서는 React의 ContextApi를 통해서 열려있는 overlay들을 관리하고 상태값에 overlay들을 랜더링하는 방식으로 이루어져있습니다.

1

2

import React, { ReactNode, createContext, useCallback, useState } from 'react';

3

4

export const OverlayContext = createContext<{

5

mount(id: string, element: ReactNode): void;

6

unMount(id: string): void;

7

onceMount(id: string, element: ReactNode): void;

8

} | null>(null);

9

10

const OverlayProvider = ({ children }: { children: ReactNode }) => {

11

const [overlays, setOverlays] = useState<Map<string, ReactNode>>(new Map());

12

13

const mount = useCallback((id: string, element: ReactNode) => {

14

setOverlays((overlays) => {

15

const temp = new Map(overlays);

16

temp.set(id, element);

17

return temp;

18

});

19

}, []);

20

21

const unMount = useCallback((id: string) => {

22

setOverlays((overlays) => {

23

const temp = new Map(overlays);

24

temp.delete(id);

25

return temp;

26

});

27

}, []);

28

29

const onceMount = useCallback(

30

(id: string, element: ReactNode) => {

31

const temp = new Map(overlays);

32

33

if (temp.has(id)) return;

34

mount(id, element);

35

},

36

[mount, overlays],

37

);

38

39

return (

40

<OverlayContext.Provider value={{ mount, unMount, onceMount }}>

41

{children}

42

{[...overlays.entries()].map(([id, element]) => {

43

return <React.Fragment key={id}>{element}</React.Fragment>;

44

})}

45

</OverlayContext.Provider>

46

);

47

};

48

export default OverlayProvider;

49

Provider 내부에서는 Map 자료구조를 통해서 key/value값 형태로 overlay들을 관리하고 있고
overlay를 등록,삭제를 하기 위한 매서드등을 만들고 하위요소에 해당 매서드들을 사용할 수 있도록 context API를 통해 공유하고 있습니다.
그리고 저장된 overlay들이 있다면 랜더링을 해주고 있습니다.

Controller

1

import { Ref, forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react';

2

3

interface OverlayControllerProps {

4

overlayElement: any;

5

onExit: () => void;

6

}

7

export interface OverlayControlRef {

8

close: () => void;

9

}

10

const OverlayController = forwardRef(function OverlayController(

11

{ overlayElement: OverlayElement, onExit }: OverlayControllerProps,

12

ref: Ref<OverlayControlRef>,

13

) {

14

const [isOpen, setIsOpen] = useState(false);

15

16

const handleClose = useCallback(() => setIsOpen(false), []);

17

18

useImperativeHandle(

19

ref,

20

() => {

21

return { close: handleClose };

22

},

23

[handleClose],

24

);

25

26

useEffect(() => {

27

requestAnimationFrame(() => {

28

setIsOpen(true);

29

});

30

}, []);

31

32

return <OverlayElement isOpen={isOpen} close={handleClose} onExit={onExit} />;

33

});

34

35

export default OverlayController;

훅으로 동작하기 때문에 컨트롤을 하기 위한 컴포넌트이고
현재 overlay가 open상태인지 close상태인지에 대한 상태값과 overlay요소에서 unMount를 할 수 있게 매서드를 넘겨주는 역할을 합니다

React의 useImperativeHandle을 통해서 ref로 접근시에 close라는 매서드를 사용할 수 있도록 열어두어 외부에서 controller 컴포넌트의 open상태값을 변경할 수 있도록 하였습니다. 그리고 전달 받았던 overlayElement에 현재 열려있는지에 대한 상태값,종료함수,삭제함수를 전달합니다
isOpen close onExit 값들을 먼저 받아 커스텀 후에 넘겨줄 수도 있습니다

이해가 되지 않는다면 다음 내용과 같이 읽는다면 이해가 될 수 있어요!

useOverlay(hook)

1

import { useContext, useEffect, useId, useMemo, useRef } from 'react';

2

import { OverlayContext } from './OverlayProvider';

3

import OverlayController, { OverlayControlRef } from './OverlayController';

4

5

const useOverlay = () => {

6

const context = useContext(OverlayContext);

7

8

if (context === null) {

9

throw new Error('not found provider');

10

}

11

12

const [id] = useId();

13

14

const { mount, unMount } = context;

15

16

const overlayRef = useRef < OverlayControlRef > null;

17

18

useEffect(() => {

19

return () => {

20

unMount(id);

21

};

22

}, [id, unMount]);

23

24

return useMemo(

25

() => ({

26

open: (overlayElement: any) => {

27

mount(

28

id,

29

<OverlayController

30

key={Date.now()}

31

ref={overlayRef}

32

overlayElement={overlayElement}

33

onExit={() => unMount(id)}

34

/>,

35

);

36

},

37

close: () => {

38

overlayRef.current?.close();

39

},

40

exit: () => {

41

unMount(id);

42

},

43

}),

44

[id, mount, unMount],

45

);

46

};

47

48

export default useOverlay;

사용하게 될 hook입니다. 이전에 만들어 두었던 context 값을 가져오고 저장할 id 값을 얻기 위해 ReactuseId를 통해 유니크 키값을 얻습니다.

context에서 overlay를 등록하고 삭제하는 매서드인 mount unMount를 가져옵니다 그리고 Controller에서 관리하는 상태 값 변경을 하기 위해 접근하기 위한 ref를 만들어 줍니다

useOverlay에서는 3개의 매서드를 Object 형태로 반환합니다.

open매서드는 mount매서드를 통해 전달받은 Element를 id값과 이전에 만들었던 Controller컴포넌트를 통해 overlay할 컴포넌트를 등록하는 매서드입니다.

close매서드는 Controller에서 열어두었던 close매서드를 ref를 통해 overlay컴포넌트를 닫는 매서드입니다.

exit매서드는 unMount를 사용하여 context에 저장된 overlay를 완전히 삭제하는 매서드입니다

사용방법은 다음과 같습니다.

1

const overlay = useOverlay();

2

3

const openOverlay = () => {

4

overlay.open(overlayElement);

5

};

overlay할 컴포넌트에 isOpen close onExit 그대로 들어가도 된다면 이렇게 사용해도 무방하지만 추가로직을 넣어야한다면 다음과 같이 사용도 가능합니다

1

const overlay = useOverlay();

2

3

const openOverlay = () => {

4

overlay.open(({ isOpen, close, onEixt }) => (

5

<OverlayElement

6

open={isOpen}

7

closeAndExit={() => {

8

close();

9

onExit();

10

}}

11

/>

12

));

13

};

slash 문서에는 Promise 예시가있는데 필자는 아직 Promise는 해보지 않았습니다.

느낀점

useOverlay를 뜯어보며 overlay들을 관리하는 방법과 Portal을 사용하지 않고 사용하는 방법에 대해서 알 게 된 것 같다 제어할 수 있도록 Controller를 생성하여 제어하는 부분이 인상깊었다 slash 라이브러리는 토스에서 사용하는 라이브러리 이므로 자주 찾아보고 어떻게 구현하였는지 살펴보면 좋을 것 같다.

profile

한동룡