11월 2주차
📌 이미지의 offsetTop 값 가져오기와 debounce를 이용한 최적화
상세 페이지는 텍스트 n개와 이미지 m개가 하나의 그룹을 이루어서 총 x개의 그룹이 보여진다. 그리고 각 그룹에 매칭되는 버튼들이 존재했는데, 특정 버튼을 누르면 버튼에 매칭되는 그룹으로 스크롤이 자동으로 이동해야 했다. 그래서 각 그룹의 offsetTop을 구해야했는데 이 offsetTop의 값이 이상하게 계산됐다.
참고로 offsetTop을 계산한다는 말은 scrollTo를 이용한다는 말인데, scrollIntoView를 이용하지 못하는 이유는 스크롤이 어떤 그룹의 위치에 있느냐에 따라서도 이 버튼의 상태에 영향을 줄수 있기 때문이다. 버튼이 스크롤 위치에 영향을 줄때 scrollIntoView를 이용하고, 스크롤이 버튼에 영향을 줄때 intersection observer를 이용하는 방법도 존재하지만, intersection observer 사용이 너무 복잡해지고 원래 사용 목적과도 어긋나기도했다. offsetTop이 있으면 스크롤이 버튼에 영향을 줄수도 있고, 버튼이 스크롤에 영향을 줄수도 있어서 로직 복잡도도 낮아진다고 생각했다.
다시 본론으로 돌아와서, 처음 페이지에 진입할때 가져오는 offsetTop 값과 페이지에 진입한 후 내부 로직이 다시 실행될 때의 offsetTop값이 달랐다. 여기서 내부 로직을 다시 실행시킨다함은(표현이 맞는지 모르겠다 -_-) 페이지에 진입한 후에 offsetTop을 구하는 useEffect 내부에 console.log(‘test’)를 작성하고 저장하는 것이다.
뭐가 문제일까해서 팀장님께 여쭈어봤더니 이미지가 렌더링되기 전에 offsetTop이 계산돼서 그렇다는 것을 알게되었다. next/image에서 onLoadingComplete 프로퍼티를 이용하면 해결할 수 있을 것 ‘같았다’.
구현하면서 아뿔사 했던게, onLoadingComplete를 이용하는 경우, next/image가 제공하는 lazy loading을 사용할 수 없게된다. lazy loading을 사용하는 경우, 실제 view가 보여지기 전까지 onLoadingComplete이 호출되지 않기때문이다.
일단은 next/image의 loading 프로퍼티를 eager로 만들어서 lazy loading이 안되게 구현하고, 모든 이미지들에 대해서 onLoadingComplete 프로퍼티에 offsetTop을 업데이트하는 함수를 전달했다. 여기에 성능적인 부분을 고려하여 업데이트 함수가 debounce되도록 했다. (이미지1이 onLoadingComplete이 되더라도 이미지2, 이미지3 onLoadingComplete가 호출되면 이미지1의 onLoadingComplete 로직 실행을 폐기한다.)
📌 next/image 최적화
이미지를 가져올때 query로 image transformation을 하지 않고, 고정된 크기에 따라서 이미지를 가져오는 URL path가 달라진다.
next/image는, static image에 대해서 deviceSizes에 따라서 resize된 이미지를 만들어서 .next에 저장하는데, 앞서 말한 상황에서는 resize되지 않은, 동일한 크기를 갖는 이미지를 만들어서 .next에 저장한다. 불필요한 파일이 저장되는 것이다.
nextjs 문서를 뒤져봐도 특정 상황에서만 deviceSizes를 가져오는 방법은 제시되어 있지 않아서, sizes 프로퍼티를 설정해서 모든 media condition에서 하나의 size만 갖도록 만들어서 이러한 문제를 해결했다.
📌 상세 페이지 리펙토링
현재 회사에서 맡고 있는 상세 페이지는 굉장히 어려운 페이지다. 어렵다고 느끼는 이유는 첫번째로는 유저가 인터랙션할수 있는 요소가 상당히 많다. 두번째로는 하나의 스크롤 핸들러 함수가 해야만하는 일이 너무 많다. 상세 페이지에는 구글 맵이 존재하고, 구글 맵 안에는 마커가 존재하는데, 마커를 누르면 스크롤 위치가 바뀌어야 하고, 스크롤 위치에 따라서 마커 위치가 바뀌어야한다. 스크롤 위치에 따라서 맨 위로가기 버튼이 보여지거나 숨겨져야하고, 스크롤 위치에 따라서 메인 이미지 대신 구글 맵으로 opacity가 바뀌어야한다. 댓글 영역으로 가기 버튼을 누르면 스크롤 위치가 바뀌어야하고, 이전과는 다르게 스크롤 위치에 따라서 마커 위치가 바뀌지 않아야한다.
복잡하다는 이야기는 여기까지하고, 리펙토링에 대해서 이야기해보면 크게 두가지 리펙토링을 했다.
1. 컴포넌트 나누기
처음 상세 페이지를 마주했을 때, 관련한 로직이 모두 하나의 컴포넌트 안에 담겨있었다. 내가 가장 먼저 한 일은 하나의 컴포넌트를 여러 개의 컴포넌트로 쪼개는 일이었다. 컴포넌트를 노드로 하는 트리 구조를 UI를 기반으로 그리고 다른 컴포넌트에 영향을 주는 상태 변화를 직선으로 표현했다. UI를 기반으로 책임을 나누고, 다른 컴포넌트에 영향을 주는 컴포넌트와 영향을 받는 컴포넌트를 최대한 가까이 배치했다.
2. 상태 관리 라이브러리에게 책임 넘기기
로직은 처음에 페이지 컴포넌트 내부에 모두 두었다. 상태 관리 라이브러리가 없었던 상태에서, 페이지 컴포넌트 내에 스크롤 핸들러 함수가 등록되는 wrapper 태그가 존재해서 어쩔 수 없었다. 그 결과 가독성이나 상태추적이 매우 어려운 상태였다. 연차가 있으신 팀장님께 여쭈었더니 로직을 Hook으로 뭉치라고 말씀해주셨다. 거대해진 Hook 두개가 만들어졌고, 상황은 크게 개선되지 않았다.
상태 관리 라이브러리가 도입되면서 이야기가 달라졌다. 사실 나는 상태 관리 라이브러리를 제대로 써본적이없다. vue 프레임워크를 사용할때 vuex를 써보기만했지(당시에도 다들 한번쯤 경험해봤듯이, 무지성으로 사용했다.) 리액트에서는 한번도 써본적이 없다. 그래서 상태 관리 라이브러리를 도입하는게 망설여졌다. 동일한 코드를 네달정도봐서 그런가 ‘현재도 괜찮은데 이걸 도입한다고해서 가독성이 개선되고 상태 추적이 쉬워질까?‘에 대한 의문도 있었다. 그림이 잘 안그려졌다. 팀장님께 여쭤봐도, 상태 관리 라이브러리 도입은 결국 또 다른 상태 추적 비용만 만들 수 있다고 말씀해주셨다.
그래서 머릿속에서는 잘 안그려지니 Before와 After를 만들기로 했다. 영향을 주는 컴포넌트와 영향을 받는 컴포넌트가 자기 자신이거나 혹은 부모 자식관계가 아니면 상태 관리 라이브러리에게 책임을 넘겼다. After를 만들고 나서도 이게 개선이 된 코드인가에 대한 의문이 존재했는데, 팀장님이나 팀원으로부터 코드가 개선되었다라는 말을 듣고 나서야 확신하게 되었다. 상태 관리 라이브러리를 유의미하게 쓴 것은 이번이 처음인 것 같다.
이러한 리펙토링 과정에서 느낀 것은, 동일한 코드를 오랫동안 보고있으면 개선 가능성에 대해서 부정적인 시각이 형성될 수 있는데, 이럴때는 Before와 After를 짜는게 가장 좋다는 것이다.
📌 네이티브앱 vs 웹앱 vs 하이브리드앱 vs 크로스플랫폼 앱 vs 프로그래시브 웹앱
네이티브 앱의 장점은 성능과 관련해서 특정 플랫폼(AOS or IOS)이 요구하는 기준을 만족해야 하기 때문에 하이브리드 앱이나 웹앱에 비해서 빠르고, 특정 플랫폼이 갖는 기능(모바일 API)을 사용할 수 있다. 또한 하이브리드 앱에 비해서 앱 스토어에 좀 더 잘 노출된다는 장점이 있다. 하지만 플랫폼에 맞는 앱 개발자를 뽑아야하고, 플랫폼에 맞는 Codebase도 두개가 존재해야 한다는 점에서 리소스가 많이 들고, 앱스토어에 배포되기 때문에 업데이트와 관련한 별도의 승인 과정이 있어서 업데이트 사항 반영이 느리다.
웹앱은 모바일 웹보다 좀 더 모바일 최적화 되어있다고한다(참고로 모바일 웹과 모바일 웹앱은 구분이 모호하다고 한다 🙄). 경우 플랫폼에 맞는 앱 개발자를 뽑거나, Codebase도 두개가 존재할 필요가 없다는 점에서 리소스가 적게 들고, 앱스토어에 배포되지 않기 때문에 업데이트와 관련한 별도의 승인 과정이 없어서 업데이트 사항을 빠르게 반영할 수 있다. 또한 브라우저를 통해서 접근이 가능하다. 하지만 특정 플랫폼의 기능을 사용할 수 없고, 네이티브앱에 비해서 느린 단점이 존재한다.
하이브리드앱은 네이티브 앱과 웹앱의 장점을 합쳐놓았다. 겉은 앱으로 감싸져있고, 내부는 웹뷰를 통해서 브라우저를 보여주는 방식이다. 웹앱처럼 플랫폼에 맞는 앱 개발자를 뽑거나 Codebase가 두개 존재할 필요가 없고, 네이티브 앱처럼 특정 플랫폼 API에 접근이 가능하다는 장점이 존재한다. 브라우저의 성능에 영향을 받으면서 네이티브앱에 비해 느리고, 웹과 앱에 대한 프로그래밍 언어적인 이해가 필요하다는 점에서 러닝커브가 높다는 단점이 존재한다.
크로스 플랫폼 앱은 하나의 언어와 한번의 개발로 IOS와 AOS 플랫폼에서 동작하는 앱을 만드는 것이다.
프로그래시브 웹앱은 모바일 웹의 한계를 브라우저의 발전을 통해서 끌어올린다. 일반적인 모바일 웹은 PWA가 되면서 ‘홈 화면에 추가’를 하면 주소창 없이 네이티브나 하이브리드 앱인것처럼 동작한다. 또한 웹의 지정된 내용들은 핸드폰에 저장되면서 통신이 안돼도 일반 웹처럼 공룡(구글 공룡게임)이 나타나는게 아니라, 웹의 오프라인 기능들이 화면에 나타난다. 안드로이드에서는 푸시 알람, 스토어 출시도 가능하다고 한다. 😮
리액트 네이티브에서 웹뷰를 띄어서 보여준다면, 크로스 플랫폼 앱이자 하이브리드 앱이라고 보면 된다고한다.
https://www.upwork.com/resources/native-hybrid-web-app-differences