최근 작업해 본 웹 뷰에 ScrollSpy UI를 적용해 보았다. 작업 후 아이폰 특정 기기에서 스크롤 높이가 정확하게 계산되지 않았는데, 상단 노치영역 높이를 고려하지 않은게 원인이었다.
해결 과정에서 아이폰은 Safe Area가 존재한다는 사실을 알게 되었다. 혹시나 나와 같이 원인을 모르고 헤매고 있을 누군가를 위하여 작성하는 글이다.
노치 영역에 대해 살펴보기 전에, ScrollSpy UI에 대해 먼저 알아보자.
ScrollSpy는 앱과 웹에서 모두 자주 사용되는 UI인데, 주로 페이지 상단에 위치해서 유저에게 현재 스크롤 위치에 대한 정보를 제공하는 역할을 한다.
위 이미지에서 가장 위 네비게이션 바 영역에 주목하자.
보면 스크롤되는 영역을 감지해서 민트색 영역(Home 영역)과 하늘색 영역(About 영역)이 교차하는 순간 About으로 탑바 하이라이팅이 옮겨간다.
또한 각 탭들(Home, About, Work, Contact)을 클릭하면 해당 탭이 active되며 그 위치로 스크롤 포인터가 옮겨간다.
정리하면 ScrollSpy UI는 두 가지의 기능을 한다.
- 스크롤 되는 높이를 계산해서 위치에 해당하는 탭 하이라이팅하기
- 각 탭 클릭시 해당 위치로 이동시키기
해당 UI가 익숙치 않으신 분들을 위해 구체적인 작동 방식을 파악해 보실 수 있는 링크 첨부합니다. 😀
다음으로는 노치영역에 대해서 알아보자.
노치 영역은 아이폰 X부터 생겨난 화면 상하단의 영역으로, 위 사진에서 오른쪽 기기의 상하단 초록색 부분이다. IOS에서 별도로 해당 영역을 제외하지 않고 웹뷰 영역을 잡게되면 노치 영역 또한 웹뷰 영역으로 잡히게 된다.
따라서 웹뷰에서는 문제없이 노출되던 영역들도 노치 영역 때문에 가려지거나 짤려 보이는 현상이 발생할 수 있다.
아래 이미지는 위에서 소개한 ScrollSpy UI중 탑바 부분만 가져온 것이다.
Home 영역을 보고 있는 상황에서 상단 탑바의 Work탭을 클릭하게 되면 해당 역역으로 이동해야만 한다. 해당 액션은 dom에서 해당 element를 잡아서 scrollIntoView 혹은 scrollTo 메서드를 사용하면 쉽게 구현할 수 있다.
하지만 노치영역이 존재하는 IOS기기에서는 노치 영역도 웹뷰 영역으로 잡히게 되기 때문에, 해당 영역의 높이를 고려하지 않으면 정확한 위치로 이동되지 않는다. 위 이미지 처럼 노치 높이만큼 화면을 위로 더 끌어올려줘야 한다. 동일한 이유로 스크롤 영역을 판별해서 액티브된 탭을 바꿔주는 위치 역시 정확하게 계산되지 않았다.
다행히도 노치 영역의 높이를 구해서 해결할 수 있었다. ios 11.2버전 이상에서는 safe area를 구할 수 있는 env() css 함수를 제공한다.
env(safe-area-inset-top)
env(safe-area-inset-right)
env(safe-area-inset-bottom)
env(safe-area-inset-left)
노치 영역을 위한 padding이나 margin 영역을 줘야할 경우, 위와 같이 css에 env 속성을 이용해서 적용할 수 있다. 노치 영역이 없는 경우 0을 반환한다.
하지만 위의 경우는 특정 탭 클릭 시 ScrollTop을 통해 높이를 직접 설정해줘야했기 때문에 노치 영역의 px값을 javascript로 받아와야만 했다.
이런 경우에는 safe-area 영역값을 css 전역변수로 정의하면 getComputedStyle을 통해 받아올 수 있다.
💡 해당 글의 코드를 참조하여 구현하였습니다.
우선 global style 변수를 아래와 같이 선언해준 후,
:root {
--sat: env(safe-area-inset-top);
--sar: env(safe-area-inset-right);
--sab: env(safe-area-inset-bottom);
--sal: env(safe-area-inset-left);
}
필요한 곳에서 해당 값을 이용해 변수로 받아온다.
const notchHeight = getComputedStyle(
document.documentElement,
).getPropertyValue("--sat");
ios simulator를 통해 웹뷰를 띄우고 콘솔을 띄워보면 47px이 출력된 것을 확인할 수 있다.
마지막으로 프로젝트에 적용된 전체 코드를 살펴보자.
const notchHeight = getComputedStyle(
document.documentElement,
).getPropertyValue("--sat");
const offset = 100 + Number(notchHeight.substring(0, notchHeight.length - 2));
const handleTabClick = (tab: string) => {
window.scrollTo({
top:
refs.current[tabList.findIndex(element => element === tab)].offsetTop -
offset,
behavior: "smooth",
});
};
각 탭 클릭시 노치 높이 + 탑바 높이 만큼 화면을 더 올려줘서 정확한 위치를 잡아줄 수 있었다.
웹뷰 작업을 하다보니 아무래도 스크롤 이벤트를 자주 다루게 된다. 특히 최근에 작업한 뷰에서는 특정 element 위치와 현재 스크롤 위치를 비교하며 처리해야 하는 로직이 많았다.
스크롤 이벤트와 ref의 조합으로 처리했는데 코드가 좀 지저분해 지는 것 같다. 더 개선할 수 있을지 한번 고민해봐야겠다.