일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- HTML
- js코테
- 리액트댓글기능
- 프로그래머스
- 리액트커뮤니티
- 프로그래머스JS
- 백준js
- dp알고리즘
- JS
- 안드로이드 스튜디오
- 알고리즘
- 코딩테스트
- 백준
- JS프로그래머스
- 코테
- css기초
- 백준구현문제
- 익스프레스
- 몽고DB
- 다이나믹프로그래밍
- 프로그래머스코테
- CSS
- 백준구현
- 백준골드
- 백준알고리즘
- 리액트
- 자바스크립트
- HTML5
- 포이마웹
- 백준nodejs
- Today
- Total
개발새발 로그
SnackBar를 구현하면서...- Context API, CreatePortal 본문
📖들어가며..
오늘은 스낵바를 구현해보려고 합니다
스낵바는 Toast와는 조금 다른 개념입니다.
Toast:
- 화면의 상단 또는 하단에 잠깐 표시됩니다. 일반적으로 화면의 중앙에 뜨지 않습니다.
- 사용자가 상호작용할 수 없으며, 자동으로 사라집니다. 사용자가 메시지를 클릭하거나 무언가를 선택할 수 없습니다.
Snackbar:
- 사용자가 버튼을 클릭하여 추가 작업을 수행할 수 있는 옵션을 제공합니다. 예를 들어, "실행 취소" 버튼을 포함할 수 있습니다.
- 화면의 하단에 표시되며, 사용자와의 상호작용을 위한 버튼을 포함할 수 있습니다.
쉽게말하면 스낵바는 별도의 액션이 가능한 Toast입니다.
Toast에서 확장이 된 것이라고 생각합니다.
아무튼 스낵바를 구현해보려고하는데 Context API와 CreatePortal을 이용해 구현해볼 것입니다.
SnackBar의 열림/닫힘/제거 흐름
먼저 설계한 스낵바의 생성, 제거 흐름입니다.
하나의 스낵바가 열릴 때 6번의 렌더링을 하게 됩니다.
1.. open일 때 스낵바가 생성되면서 렌더링됩니다.
2. enter라는 클래스가 useEffect를 통해 state로 할당되면서 스낵바가 화면에 나타나게 되고, 렌더링이됩니다.
3. 스낵바가 나타나면 onAnimaitonEnd를 통해 show라는 클래스가 할당되고, 스낵바가 화면에서 고정됩니다. 렌더링이됩니다.
4. timeout내부의 함수가 시간이 경과되어 실행되고, 스낵바의 isOpen값을 false로 만듭니다. 렌더링이 됩니다.
5. isOpen이바뀌면 다시 useEeffect의 의존성 배열로 인해 className에 exit를 추가해 사라지는 애니메이션을 실행합니다. 렌더링 됩니다,
6. 스낵바가 사라지면 onAnimationEnd를 통해 스낵바 제거를 수행합니다.
위 과정이 불필요해보이고 복잡해보입니다.
위 과정을 통해 리팩토링도 아래에서 진행해봤습니다.
1. Context API로 구현하는 방법
먼저 Context API로 구현하는 방법입니다.
간단히 설명하면
snackbarContext는 실제 스낵바의 데이터를 관리합니다.
snackbarSetContext는 스낵바의 데이터를 업데이트하거나 삭제하는 Dispatch함수를 담당합니다.
스낵바 데이터와 Dispatch함수는 useReducer의 [state, disptach]를 의미합니다.
Context를 둘로 나눈 이유는?[공식문서]
만약 Dispatch함수만 사용하는 컴포넌트가 존재하는데 Context가 둘로 나누어져 있지 않다면 다른 컴포넌트들도 불필요하게 리-렌더링을 하게됩니다.
이를 위해서 둘로 나눈 것입니다!
그리고 해당 컨텍스트를 사용할 수 있도록 useSnackBar와 useSetSnackbar를 정의해 export해줍니다.
- useSnackBar는 Context를 그대로 useContext()합니다.
- useSetSnackbar는 갖고있는 Disptach함수로 createSnackBar와 removeSnackBar를 만들어줍니다.
- 이때 useSnackbar()통해 snackBarContext에 있는 스낵바 데이터를 가져옵니다.
- 스낵바 데이터를 활용해서 createSnackBar와 removeSnackBar 기능을 만듭니다
이제 ContextProvider에 내려주는 state와 disptach를 갖고있는 useReducer를 구성한 방법입니다.
위 설명을 간단히하면 useReducer의 첫 번째 인자로 들어가는 reducer에 snackBarReducer()를 넣어줍니다.
- snackBarReducer()는 단지 type에 따라 snackbarReducerMap을 호출합니다.
- snackbarReducerMap{}은 type에 따라 매핑해서 upsert 또는 remove를 수행하는 객체입니다.
그 안에서는 upsert와 remove에 저장된 함수가 있고, 함수에서 기능을 수행한 후 데이터를 반환합니다.
Provider를 사용한 부분은 아래와 같습니다.
그럼 해당 데이터를 사용해서 스낵바를 띄울 공간을 만들어야 하는데 이 부분이 SnackBarRoot입니다.
모든 스낵바 데이터를 map으로 나열합니다.
이때 SnackBarItem에서는 해당 데이터를 가지고 enter, show, exit 애니메이션을 수행해야합니다.
여기서 중요한 부분은 onAnimationEnd를 통해서 스낵바가 show하거나 exit한 이후 수행할 로직을 간단하게 짤 수 있다는 것입니다.
최종적으로는 아래와 같이 사용하면 간단하게 스낵바가 나타나게 됩니다.
여기까지 간단하게 ContextAPI로 구현한 스낵바였습니다.
이 방식의 장단점은
장점
- 스낵바 데이터들이 한 번에 관리되므로 추가 확장 시 용이합니다.
- 예를 들어 스낵바 갯수를 제한할 수 있습니다.
단점
- Context Provider를 꼭 사용해줘야 합니다.
- 6번의 렌더링이 발생합니다
위 처럼 정리할 수 있습니다.
2. 커스텀 훅과 CreatePortal을 이용한 방법
이제는 커스텀 훅으로 스낵바를 간단하게 사용하고, 스낵바는 createPortal로 띄우는 방법을 보겠습니다.
이 방식도 Context API와 마찬가지의 흐름을 갖고 있습니다.
위처럼 useSnackBar() 훅 내부에서 스낵바 하나의 상태를 관리하고,
이를 createPortal을 이용해 지정된 곳에 portal 시키는 것입니다.
그리고 해당 훅을 아래와 같이 사용하면 됩니다.
간단하게 useSnackBar 훅으로 스낵바를 만들 수 있습니다.
하지만 이 방법에도 장단점이 존재합니다.
장점
- useSnackBar사용으로만 간단하게 사용할 수 있습니다.
- Provider없이 가능합니다
단점
- 사용시 id가 "snackBarRoot" 인 컴포넌트가 필수인 것을 개발자가 인지해야 합니다.
- 여전히 6번의 렌더링이 발생합니다.
- useSnackBar가 반환한 snackBar 데이터를 꼭 JSX에 명시해야하는번거로움이 있습니다.
3. 단점을 해결한 useSnackBar
먼저 appendChild()를 이용해 <SnackBarWrapper/>와 같은 스타일을 가진 div 엘리먼트를 생성합니다.
그리고 중복 생성이 되지않도록 합니다.
처음에는 사실 createRoot를 사용했지만 공식문서에 보이듯이 남용을 해서는 안되는 것으로 판단했습니다.
CreateRoot를 남용해서 사용해도 될까?[공식문서]
createRoot를 호출하면 브라우저 DOM 엘리먼트 안에 콘텐츠를 표시할 수 있는 React 루트를 생성합니다.
주의 사항
- 앱이 서버에서 렌더링 되는 경우 createRoot()는 사용할 수 없습니다. 대신 hydrateRoot()를 사용하세요.
- 앱에 createRoot 호출이 단 하나만 있을 가능성이 높습니다. 프레임워크를 사용하는 경우 프레임워크가 이 호출을 대신 수행할 수도 있습니다.
- 컴포넌트의 자식이 아닌 DOM 트리의 다른 부분(예: 모달 또는 툴팁)에 JSX 조각을 렌더링하려는 경우, createRoot 대신 createPortal을 사용하세요.
그리고 useSnackBar 훅 내부에서는 아래와 같이 반환합니다.
이를 통해 기존에 문제가 되던 " 사용시 id가 "snackBarRoot" 인 컴포넌트가 필수인 것을 개발자가 인지해야 합니다." 를 해결했습니다.
이제 사용자는 그저 useSnackBar()를 아래와 같이 사용하기만 하면 됩니다.
그럼 이제 6번 렌더링되는 부분을 개선해보겠습니다.
일단 open - enter - show - exit - remove으로 진행되는 렌더링 순서를
show - exit - remove로 바꿔보겠습니다.
기존에 사용되던 SnackBarItem의 구조를 바꿔야합니다.
SnackBar아이템은 컴포넌트가 생성되어 호출됐을 때 바로 나올 수 있도록 합니다.
스낵바를 구성하는 Container 인데 위처럼 show값이 true면 애니메이션이 발생하도록 했습니다.
그러면 버튼을 눌러 SnackBarItem이 생성되어 호출됐을 때 위 애니메이션이 바로 발생하게 됩니다.
그리고 SnackBarItem 내부에서는 스낵바의 노출 시간이 경과하면 show 상태를 false로 만들어줍니다.
그리고 다시 setTimeout을 걸어 실제로 스낵바를 사라지게 하면 됩니다.
즉 SnackBarItem은 생성과 동시에 타이머가 돌아가고,
일정시간이 되면 show 상태가 false가 되어 사라지는 애니메이션이 작동하게 되고,
이후 삭제가 됩니다.
이를 통해서 3번의 렌더링만 하게 되었습니다.
여기까지 간단하게 문제점을 개선해보았습니다.
추가적으로 사용자 경험을 위해 ProgressBar 기능도 추가하였는데 이 부분은 따로 설명하지 않겠습니다.
아래와 같이 시간 경과되는 것이 화면에 보이고, 마우스를 올렸을 때 같이 멈추는 기능도 구현해봤습니다.
📘마무리하며..
스낵바 기능을 구현하면서 좀 더 사용자 경험에 적합한 기능을 구현하는 것에 대해 많이 배울 수 있게 되었습니다.
특히 ProgressBar 기능을 구현하면서 같이 시간이 멈춰야하지 않을까?라는 생각을 했고, 이를 해결하는 시간이 꽤 걸렸습니다.
그리고 사실 구현한지는 꽤 됐는데 면접이랑 과제하느라고 이제서야 올리게 됐습니다...
그래서 다시 어떻게 기능하는지 살펴보게되었고,
다시금 문제점을 확인하게 개선하는 상황도 있었습니다.(CreateRoot 남용)