일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- JS프로그래머스
- 포이마웹
- 코테
- JS
- 알고리즘
- CSS
- dp알고리즘
- 안드로이드 스튜디오
- 프로그래머스코테
- 코딩테스트
- 리액트댓글기능
- 백준골드
- 리액트커뮤니티
- 백준구현
- 자바스크립트
- 프로그래머스JS
- css기초
- 리액트
- js코테
- 백준구현문제
- 백준js
- 몽고DB
- 다이나믹프로그래밍
- 백준알고리즘
- HTML
- 프로그래머스
- 백준nodejs
- 익스프레스
- 백준
- HTML5
- Today
- Total
개발새발 로그
React - Modal 기능을 구현하면서 본문
📖들어가며..
24년 2월 23일에 처음으로 useModal을 만들기로 계획했다.
Modal을 사용하려면 Modal이 열릴 컴포넌트와 열리고 닫히는 상태를 반복적으로 선언하고 사용하는 부분에서 불편함을 느꼈었다.
그래서 useModal이라는 커스텀 훅을 개발하려고 마음먹고 시작했다.
이때는 "Modal을 구현할 때는 createPortal을 활용하면 좋다."는 부분과 "재사용성 높은 Modal"에 꽂혀 혼자서 이것저것 구현하고 있었다.
하지만 해당 코드에는 많은 문제점들이 있었다.
24년 3월 7일에 문제점을 인지하고 리팩토링을 계획했다.
문제점은
1. css의 display : none을 사용해서 Modal이 보이거나 사라지는 부분은 좋지 않다.
2. useModal 훅 내에서 ModalComponent를 초기화 중인 부분은 좋지 않다. (useModal이 계속 초기화 됨)
3. Modal이 특정 상황에서 keyframe 애니메이션 효과가 이상하게 동작하는 상황이 발생했다.
4. SSR에서는 document를 호출하지 못하는 문제가 있다.
해당 문제를 해결하기 위해서
1. Modal 오픈 시에는 transform이나 opacity 속성을 사용
2. ModalComponent와 Modal 상태를 관리하는 state를 분리
3. transtion 효과가 올바르게 동작하도록 Modal이 열려있을 때만 동작하게 함.
4. document가 호출될 수 있는 환경에서만 호출하게 함
위와 같이 사실상 문제를 덮으려고만 하는 해결책을 선택했다.
핑계이지만 프로젝트 기간이라서 사용자가 사용할 수 있는 프로토타입까지는 빠르게 구현해야했기에 시간을 많이 쏟아붇지 못했다.
해당 문제점은 프로젝트 진행내내 생각이 났고, 이를 해결하기 위해 useModal 리팩토링을 계획했다.
24년 3월 31일, 사실상 프로젝트 기간이 끝나고, 이후 리팩토링 기간에서 나는 먼저 Modal을 보통 어떻게 만드는지 조사하고, 옳은 방법은 무엇인지 찾게 되었다.
최적의 방법을 사용해 이후에 useModal이라는 기능을 만들어서 어디에서나 사용할 수 있도록 모듈화를 하고 싶어졌기 때문이었다.
해당 기간 동안에는
1. 조건부 렌더링으로 만드는 Modal의 장점과 단점
2. css속성 translate 또는 opacity를 이용한 Modal의 장점과 단점
3. 내가 개발할 Modal은 위 두가지 방법 중 어떤 것을 선택해야하는지
4. Context로 Modal을 관리하는 방법
등을 집중적으로 개선해보려고 했다.
결과적으로는 조건부렌더링을 선택했고,
조건부 렌더링의 단점 중 한가지인 transition 효과가 적용되기 어렵다는 점은
setTimeout을 이용해 transition 효과가 모두 진행되도록 했다.
또한 setTimeout 사용으로인해 Modal 열기 버튼을 연속적으로 클릭하면 Modal의 열리고 닫힘이 꼬이는 현상을 해결하기 위해 Modal이 열리고 있는 중인 Loading 상태(useRef를 이용)도 넣어주었다.
이렇게 끝나나 싶었던 현재
1. setTimeout을 이용해 transition 효과를 나게하는 것이 과연 옳은 것인가?
2. Modal에서만 [ Modal의 실제 열림/닫힘 상태, Modal의 transition 효과를 위한 상태, transition효과가 꼬이지 않기 위한 Loading 상태 ], 3가지의 상태가 관리되고 있는지 과연 옳은 것인가?
와 같은 개인적으로 생각한 문제점들이 도출되었다.
나는 사실 어떤 기능을 개발하던지 이게 옳은 방법일까?가 가장 고민되는 부분이다.
✨다시 시작된 useModal 리팩토링
이제 위에서 고민되는 것을 해결하기 위해 다시 한번 useModal을 어떻게 옳은 방법으로 만들 수 있을까를 생각하면 리팩토링을 진행했다.
🚨눈엣가시 setTimeout의 존재
Modal이 조건부 렌더링으로 열림/닫힘이 진행되기 때문에 transition 효과를 적용하기가 어려웠다.
그래서 setTimeout을 이용해서 아래와 같은 순서로 진행하게 했다.
1. 열림 클릭 시 Modal이 DOM트리에 생성됨
2. 하지만 Modal은 보이지 않는 상태(opacity 또는 transform으로 화면에 보이지 않음)
3. 이후 setTimeout에서 실제 Modal이 보이도록 상태 변경
4. Modal이 나타남
즉 Modal 열림 -> 실제 Modal 보이게 열림 순으로 열리는 것이다.
물론 닫힐 때도 실제 Modal 안보이게 닫힘 -> Modal 닫힘 순이다.
당연히 렌더링도 두 번 일어나게 된다.
이 방식은 보통 앱에서 활용하는 방식인 것 같았다.
예제로 중고나라 모바일 앱에서는 Modal을 열 때 Modal 열림 -> 실제 Modal 보이게 열림 순으로 열리는 것을 확인했다.
아무튼 이제 저 setTimeout을 사용하는 방식을 없애보려고한다.
솔직히 너무 짜친다.
일단 기존에 useModal에서 Modal 열림 상태, 실제 Modal 보이게 열림 상태를 분리해야한다.
useModal에서는 Modal 열림상태(isOpen)을 관리하고,
Modal 컴포넌트에서는 실제 Modal이 보이게 열림 상태(isAnimating)를 관리하게 했다.
그리고 Modal 컴포넌트에서는 props로 받아오는 isOpen 값이 바뀌고, 만약 true(열림)이라면 useEffect를 이용해서 isAnimating을 바꿔서 다시 렌더링하게했다.
그럼 문제가 하나 생긴다.
🤔닫힐 때의 순서 실제 Modal 안보이게 닫힘 -> Modal 닫힘은 어떻게 할 것인가?
지금까지 Modal 열림 -> 실제 Modal 보이게 열림 순을 구현하기 위해 위 처럼 진행했다.
닫힐 때는 Modal이 transition으로 화면에서 사라지고, 이후 Modal이 DOM트리에서 제외되어야 한다.
이 때문에 setTimeout을 사용했었다.
이 부분에서 많은 고민을 했었는데 transitionEnd라는 이벤트가 있다는 것을 알게되었다.
그리고 React에서도 onTransitionEnd 이벤트를 지원하고 있었다.
추가로 여러 transition 효과가 겹치는 상황이 있기 때문에 모범 사용사례도 있었다.
그래서 위처럼 onTranstionEnd를 이용해서 transition이 끝나면 isOpen 상태를 false로 바꾸는 함수를 실행하게 했다.
그리고 useModal은 아래와 같이 바뀌었다.
결과적으로 setTimeout을 사용한 방식을 완전히 제거할 수 있었다.
단점이라고 한다면 Modal이 열릴때 2번의 렌더링과 닫힐 때 2번의 렌더링이 발생한다.
이 단점이 굉장한 단점이라고도 생각이 든다.
왜냐하면 모달 하나에 4번의 렌더링을 하게 되는 것이니까
그렇지만 조건부 렌더링을 사용해야한다는 조건과 transition효과가 적용된다는 조건이 성립하기 위해서는 이 같은 방식이 맞다고 생각이 들었다.
하지만 transition을 적용하고 다른 문제가 발생했다.
💢트러블 슈팅
🤔왜 useEffect로 Modal을 열면 transition효과가 나타나지않을까?
나는 위 코드처럼 어떤 페이지에 도달하면 useEffect를 이용해서 바로 모달이 열리게끔 했다.
Modal을 열리게하는 open() 을 실행시켜 단순히 열리게 하는 것이 목적이었다.
나는 어차피 버튼을 클릭해서 open() 하는 거나 useEffect에서 open() 하는 것이나 동일하게 동작할 것이라 예상했다.
하지만 아래와 같이 열릴 때 내가 원하는 transition 효과가 나타나지 않았다.
처음에는 라우트 이동으로 인한 문제인 줄 알았지만 아니었다.
새로고침을 했을 때도 위와 같은 문제가 발생했다.
그래서 원인을 찾기위해 이것 저것 시도해보았다.
🤔개발자 도구를 열고 실행하면 잘 되는 현상?
기본 구글 크롬에서 개발자 도구를 열었을 때 transition효과가 갑자기 적용이됐다.
코드가 전혀 바뀌지않았는데 말이다.
그래서 시크릿 탭을 이용해서 개발자 도구를 연 상태로 똑같이 진행했더니
다시 transition 효과가 적용되지않은 상태로 나왔다.
원인은 react-dev-tools 때문인 것 같았다.
근데 왜 react-dev-tools 때문인 걸까?
그래서 시크릿 탭에서 아래처럼 cpu 감속을 주고 다시 실행해보았다.
그랬더니 transition 효과가 적용된 상태로 동작하게 되었다.
또 특이한 점은 x6 감속일 때는 transition이 적용되지 않고, x20일 때만 적용이 되는 것이었다.
🤔그렇다면 도대체 무엇이 문제일까?
먼저 테스트를 하던 도중 이상한 부분때문에 무슨 문제인지 명확하게 파악하기가 어려웠다.
그래서 예상하기로는
isOpen이 바뀌는 첫 번째 렌더링
isAnimating이 바뀌는 두 번째 렌더링
두 렌더링이 한번에 일어나는 것처럼 보이는 것으로 이해했다.
왜 일어나는 것처럼 보이는 것이라 표현했냐면
cpu 감속이나 react-dev-tools로 정상적인 transition이 보이는 것을 보면 무엇인가 렌더링을 빠르게 표현하는 것 같았다.
😁그래서 챗지피티님을 이용해봤다.
그랬더니 아래와 같은 답변을 해주었다.
나는 일단 위 react-dev-tools에서 봤듯이 2번의 렌더링, 최초 렌더링까지 합쳐서 3번의 렌더링이 일어난다.
근데 여기서 최초렌더링을 제외한 2번의 렌더링
isOpen이 바뀌는 첫 번째 렌더링
isAnimating이 바뀌는 두 번째 렌더링
이 렌더링이 너무 빠르게 변해서 브라우저는 하나의 렌더링으로 볼 수 있다고 한다.
근데 사실 이게 맞는지는 모르겠다.
상태변화가 빨리 일어난다고 브라우저가 이걸 하나의 렌더링으로 본다고..?
이해가 잘 안갔다.
리액트에서는 3번의 렌더링을 하는데 브라우저는 이걸 2번만 한다는 것이다.
근데 또 어떻게 보면 맞을 수 있는 게 위에서 했던
CPU감속을 x20 했을 때 transition이 제대로 나오는 걸 보면 그럴 수 있다고 생각했다.
일단 아래와 같은 결론을 받고 이해하기로 했다.
추후 더 조사해서 이게 진짜로 맞는지 확인해봐야겠다.
🤔어떻게 해결해야할까?
🛠️setTimeout
이를 해결하기 위해서는 간단하게 아래와 같이 할 수 있다.
setTimeout으로 강제로 텀을 두게 하고, transition 효과가 일어날 수 있게 했다.
특이한 점은 시간을 0으로 두면 안된다.
그래서 일단 랜덤한 값으로 20을 주었다.
만약 1을 주면은 빠르게 클릭했을 때 다시 transition 효과가 안나타나는 경우가 생기기도 했다.
이 때문에 더욱이 무슨 문제인지 파악이 어려웠다.
정말 "transition 효과를 일으킬 시간이 없을만큼 빠르게 렌더링 되버린 것인가?" 라는 추측만 생각됐다.
+ 추가로 transition의 시간을 `3s` 처럼 긴 시간으로 바꿔봤는데 이건 상관이 없었다. transition은 여전히 안나타났다.
🛠️rAF
rAF를 사용한 방법도 있다.
requestAnimationFrame을 사용하여 상태 변경 후 브라우저의 다음 렌더링 프레임에서 isAnimating을 true로 설정한다.
이렇게 하면 브라우저가 트랜지션 효과를 인식하고 제대로 적용할 수 있다.
만약 다른 방법으로 해결하자면
🛠️react-transition-group
아까 위에서 언급한 라이브러리다.
이를 활용하면 간단하게 페이지 이동간에 transition이 가능하다고 한다.
또 다른 방법이라면 View Transition API가 있다.
🛠️ View Transition API
이건 실험적인 API라 표준 스펙은 아니지만 활용하기에 아주 좋은 기능이라 생각됐다.
특히 react-router-dom에서도 이를 활용할 수 있도록 해놓았다.
🛠️unstable_viewTransition
이게 View Transition API을 기반으로 하는 것이라고 한다.
그래서 지금 내 코드에서도 아래와 같이 작성하면 페이지 이동간에 transition이 적용된다.
transition 효과가 opacity같은 걸로 고정되어있지만 커스텀도 가능한 것처럼 보였다,.
🚨다시 사용되는 setTimeout..
위에서 언급한 setTimeout이나 rAF 방법을 이용할 수 있었지만 나는 되도록이면 둘을 사용하고 싶지 않았다.
뭔가 불필요해보이고, CPU감속을 줬을 때 transition이 보이는 것이면 해결방법이 있을 것 같았다.
그 결과 나는 transition이 아닌 animation을 사용하기로 결정했다.
animation을 사용하면 트랜지션과 달리 animation은 상태 변경이 발생하지 않아도 시작할 수 있기 때문에, 상태 변경이 너무 빠르게 일어나더라도 애니메이션을 보장할 수 있다.
animation을 사용하는 것이 트랜지션 효과를 보다 안정적으로 적용할 수 있는 경우가 많습니다.
특히 상태 변경이 매우 빠르게 일어나는 경우 animation은 애니메이션을 명확히 정의하고 적용할 수 있어 트랜지션보다 안정적일 수 있습니다.
animation은 스타일의 변화를 명확히 정의하고, 상태 변화와 무관하게 애니메이션을 제어할 수 있는 장점을 제공합니다.
그래서 아래와 같이 바꾸었다.
그저 transition으로 했던 방식을 keyframes를 이용해 애니메이션으로 만들어 적용했다.
그러면 아래와 같이 onTransitionEnd를 onAnimationEnd로 바꿔줘야 한다.
그래야 애니메이션이 끝났을 때 ModalComponent를 DOM트리에서 제거할 수 있다.
이를 통해 setTimeout을 사용하지 않고, Modal 기능이 어떤 상황에서든 애니메이션 효과가 날 수 있게 됐다.
📷결과
+ 🤔Dialog를 이용한 모달?
찾아보니 <dialog> 태그를 이용해서 모달을 만들 수 있는 방법이 있었다.
이 방법이 사실상 가장 옳은 방법이라 생각이 들기도 했다..
상태를 관리하지 않아도 되는 방법이라니..!
이후 Dialog를 이용한 방법도 추가로 만들어서 모듈화를 해보려고한다.
당연히 transition 효과를 어떻게든 적용해서!
📘마치며..
이렇게 useModal을 또 한번 개선해보았다.
개선이 아닐 수도 있지만 setTimeout을 사용한 방식을 제거했다는 점에서 나는 정말 뿌듯했다.
나중에 추가 부분에 작성한 Dialog를 이용한 방식도 공부하고, 개발하려고 한다.
내가 만든 모달을 여기서 확인할 수 있다!
위 네비게이션에서 도감을 눌러 요소들을 클릭하면 확인할 수 있다.
'React' 카테고리의 다른 글
SnackBar를 구현하면서...- Context API, CreatePortal (0) | 2024.11.22 |
---|---|
ScrollSpy를 구현하면서 - 합성 컴포넌트 패턴, Render Props 패턴 (0) | 2024.11.01 |
Tooltip에 Viewport 자동 위치 조정 기능 추가하기 (4) | 2024.08.28 |
React - addEventListener와 onClick을 같이 사용해도 되나요? (0) | 2024.08.24 |
react-beautiful-dnd 라이브러리를 사용하면서.. (0) | 2024.07.31 |