컨셉비는 모바일을 주로 타겟팅하는 웹 서비스이다. 아이디어를 공유하고 토론하며 프로젝트, 공모전, 창업 등 다양한 직군을 구인할 수 있는 서비스이다. 현재는 아이디어를 공유, 토론할 때 댓글 및 답글 기능을 사용한다. 핵심 기능인만큼 사용성이 중요한데 개선해볼 요소로 크게 다음과 같이 볼 수 있겠다.
React-Query의 useSuspenseInfiniteQuery
와 React-Use의 useIntersection
을 활용하여 구현하였다. 구현 방법은 다른 곳에서도 쉽게 확인할 수 있기에 자세하게 적지는 않겠다.
요약하면 댓글 및 답글 영역 맨 하단에 div
요소를 두어 useRef
의 ref
를 할당한 뒤 useIntersection
을 통해 화면에 감지되었을 때 useSuspenseInfiniteQuery
의 fetchNextPage
함수를 실행해 추가 조회하는 방식으로 진행했다.
결과는 다음과 같다.
'더 보기'와 같은 버튼을 반복적으로 누르는 피로감 없이 간편하게 스크롤함으로써 댓글 및 답글을 추가 조회할 수 있다.
모바일 타겟팅 서비스는 디바이스 OS 및 브라우저 버전 별로 반응형 대응이 중요한데, 특히 더 신경써야 하는 부분은 가상 키보드(이하 키보드)를 사용할 때이다. 모바일에서 무언가를 입력할 때 키보드가 하단에서 올라오게 되는데, 키보드 영역만큼 화면을 가리다보니 입력창이 키보드에 가려지는 등의 문제점이 발생할 수 있다.
키보드는 IOS와 Android에 따라 동작 방식에 차이점이 있을 뿐만 아니라 사용자의 위치에 따라 키보드로 인해 요소가 가려질 수도 있고 아닐 수도 있다. 아무튼 우리 서비스에서 발생한 문제점은 다음과 같다.
위와 같이 댓글 또는 답글 입력창을 누르면 하단에 빈 공백이 생기는 것이다. 위 영상은 IOS 환경에서 녹화된 것인데 보통 IOS 키보드 문제점이라하면 입력창이 키보드 뒤로 가려져 사용자가 스크롤을 한 번 조작해줘야하는 불편함이 대부분이다.
하지만 컨셉비의 경우 Android, IOS 모두 키보드 영역만큼 댓글 입력창이 올라가긴 하나, 하단에 불규칙적으로 공백이 생기고 키보드가 사라진 뒤로도 남아 있는 문제점이 발생했다.
첫 번째로 시도해봤던 것은 window.innerHeight
와 window.visualViewport.height
를 활용한 Viewport 높이 조절이다. Android와 IOS는 키보드가 등장할 때 크게 다음과 같은 차이점이 있다.
간단하게 요약하면 Android는 window.innerHeight
와 window.visualViewport.height
값이 키보드를 뺀 나머지 영역으로 동일하지만 IOS는 window.innerHeight
는 전체 영역, window.visualViewport.height
는 키보드를 뺀 나머지 영역이 되겠다. 따라서 다음과 같이 input 입력창이 하단에 있다면 키보드가 등장했을 때 가려질 수가 있다.
따라서 다음과 같이 브라우저의 resize 이벤트를 감지하여 스크롤이 생기는 Element의 style.height
값을 window.visualViewport.height
로 할당하여 영역을 조절해보았다.
// mobileViewRef: 스크롤을 조작할 수 있는 rootElement
const resizeVisualViewPortResize = (mobileViewRef: MutableRefObject<HTMLElement | null>) => {
if (!mobileViewRef.current) return;
const currentVisualViewHeight = window.visualViewport?.height;
if (!currentVisualViewHeight) return;
mobileViewRef.current.style.height = `${currentVisualViewHeight}px`;
};
useEffect(() => {
if (!window.visualViewport) return;
const windowVisualViewPort = window.visualViewport;
const handleVisualViewPortResize = () => {
resizeVisualViewPortResize(mobileViewRef);
};
windowVisualViewPort.addEventListener('resize', handleVisualViewPortResize);
return () => {
windowVisualViewPort.removeEventListener('resize', handleVisualViewPortResize);
};
}, [mobileViewRef]);
하지만 기능은 정상 동작하나, 여전히 하단에 공백이 생겼으며 기능을 적용하기 전과 큰 차이가 없었다.
이 때부터 아이폰 시뮬레이터를 깔고 여러 가지 디버깅을 해보았다. 참고로 맥북과 아이폰을 사용한다면 아이폰 시뮬레이터를 활용한 디버깅을 시도해보길 추천한다. 아이폰 시뮬레이터를 활용한 디버깅 방법은 여기를 참고해보면 좋을 것 같다. 개인적으로 아이폰으로 localhost를 직접 들어가서 테스트 하는 것보다 더 나은 것 같다.
이곳 저곳 자료를 찾아보니 resize 이벤트로 window.visualViewport.height
값을 스크롤이 발생하는 Element의 height 값으로 할당하여 해결하는 방법은 보통 IOS에만 해당되었다.
그런데 우리의 서비스는 Android와 IOS 모두 문제가 발생한다는 것에 집중했었고, 여러 번의 테스트 끝에 문제의 원인을 확인할 수 있었다. 원인은 바로 전임자 분께서 작성해두신 아래의 스타일 코드 때문이었다. 🫠
export const globalStyles = css`
#root {
height: 100vh;
width: 100%;
margin: 0 auto;
}
body {
margin: 0;
display: flex;
place-items: center;
overflow: hidden;
min-height: 100vh;
min-height: -webkit-fill-available;
user-select: none;
}
// (중략)
`
여기서 root의 height와 body의 min-height를 100vh로 고정해둔 것을 볼 수 있는데, 이 때문에 window.innerHeight
가 min-height 값으로 고정됨으로써 키보드가 등장할 때 화면이 줄어들지 않고 찌그러지는 형태가 되는 것이었다.
따라서 위 코드를 제거해주니 다음과 같이 정상적으로 동작하는 것을 확인할 수 있었다.
resize 이벤트 핸들러를 통해 따로 window.visualViewport.height
을 활용하여 값을 조절하진 않았다. 해당 로직이 없어도 정상적으로 키보드 영역만큼 요소의 높이가 조정되었기 때문이다.
window 대신 별도의 rootElement(rootLayout)를 사용하여 해당 컴포넌트 내부에서 구현한 점이 별도의 resize 로직 없이 요소의 높이를 조절할 수 있었던 요인으로 추측하고 있다. 🤔
컨셉비는 답글 입력창이 하단에 고정되어 있는 형태가 아닌 다음과 같이 각 댓글 마다 답글 입력창을 생성할 수 있다.
댓글 입력창은 상단에 고정되어 있고 클릭 시 다음과 같이 확대된다.
따라서 키보드 영역에 따라서 스크롤이 발생하는 Element의 height를 동적으로 조절해도, 사용자가 키보드를 연 위치에 따라 키보드로 인해 댓글 및 답글 입력창이 가려질 수도 있다. 즉 선택한 입력창이 화면에 보이도록 따로 포커싱 하는 로직이 별도로 필요했다.
React의 useRef를 활용하여 선택한 입력창을 focus()
하도록 하였다. 이 로직 자체로도 어느 정도 포커싱이 가능하나 다소 불규칙적이고 키보드에 입력창이 가려지는 상황도 적지 않게 발생했다. 따라서 스크롤 위치를 직접 조작할 필요가 있어보였다.
첫 번째로 시도한 방법은 다음과 같이 선택한 입력창의 절대 위치를 구하여 해당 위치로 rootElement 스크롤을 조작해 포커싱을 하는 방법이었다.
// 댓글 / 대댓글, (대)댓글수정 입력창의 위치에 영향을 주는 요소가 서로 달라 위치 조정값 분리 및 상수화
const COMMENT_DIFF = 58;
const RECOMMENT_DIFF = 108;
// textareaRef: 선택한 댓글 또는 답글 입력창
// mobileViewRef: 스크롤을 조작할 수 있는 rootElement
const focusTextareaRef = ({ textareaRef, mobileViewRef, isInComment }: FocusTextareaRefProps) => {
if (!textareaRef.current || !mobileViewRef.current) return;
textareaRef.current.focus();
const textareaRect = textareaRef.current.getBoundingClientRect();
const difference = isInComment ? RECOMMENT_DIFF : COMMENT_DIFF;
// MobileView 컴포넌트의 스크롤 Top(가장 상단)을 유저가 선택한 (대)댓글 입력창 위치의 절대값 - 위치 조정값으로 이동
mobileViewRef.current.scroll({
top: mobileViewRef.current.scrollTop + textareaRect.top - difference,
behavior: 'smooth',
});
};
위 로직은 다음과 같이 선택한 입력창을 최상단으로 끌어올려준다.
처음엔 괜찮은 방법이라고 생각했는데 위 방법이 과연 댓글 및 답글을 입력하는데 사용성이 괜찮은지 고민이 됐다. 댓글 또는 답글을 달면서 다른 사람이 먼저 단 내용을 참고하면서 달 수도 있기 때문에, 위 방식은 이런 경우에 오히려 불편할 수 있겠다.
또한 사용자가 보고 있던 입력창이 자신의 의도와는 상관 없이 최상단으로 올려지면서, 자신이 선택한 댓글에 답글을 잘 달고 있는지 등 확인 동작을 추가로 행해야할 수도 있을 것 같았다.
따라서 두 번째로 시도해본 방법은 다음과 같이 scrollIntoView
를 활용하는 것이었다.
// textareaRef: 선택한 댓글 또는 답글 입력창
const focusTextareaRef = (textareaRef: MutableRefObject<HTMLTextAreaElement | null>) => {
if (!textareaRef.current) return;
textareaRef.current.focus();
textareaRef.current.scrollIntoView({ block: 'center', behavior: 'smooth' });
};
선택한 입력창의 절대 위치를 구하여 해당 위치로 rootElement 스크롤을 조작해 포커싱을 하는 방법에 비해 코드가 상당히 간결하다. 이는 scrollIntoView
가 포커싱한 요소를 화면에 보이도록 스크롤을 자동으로 조절해주기 때문에 그렇다. 결과는 아래와 같다.
댓글 및 답글 입력창이 보다 자연스럽게 열리는 것을 확인할 수 있다. 만약 답글 입력창 표시 공간이 부족할 땐 최상단으로 올려주는 것 또한 확인해 볼 수 있다.
PC와 모바일 환경 모두 댓글 및 답글 무한스크롤, 포커싱 기능이 정상적으로 동작한다. 스크롤 포커싱 방식은 추후 팀원들과 의논 후 첫 번째 방식으로 롤백이 될 수도 있을 것 같다. 아무래도 자동으로 입력창을 포커싱하다 보니 댓글 입력창이 키보드에 살짝 가려지는 형태 때문에 불편해 하는 사용자가 발생할 수도 있다.
입력창의 여백 등을 고려해 difference
값을 조정해서 스크롤을 과도하게 최상단으로 올리지 않고 자연스럽게 조작한다면 더 세밀하고 좋은 사용성을 보여줄 수도 있을 것 같다. 이 부분은 추후 리팩토링 해보고자 한다. 😇
고려하지 못한 몇몇 엣지 케이스에서 스크롤 포커싱 미동작 오류와 ScrollIntoView의 다소 부자연스러운 포커싱 동작 때문에 리팩토링을 진행했다. 리팩토링 글은 아래에서 확인 가능하다.👇