Next.js 14, Tailwindcss, Typescript
터치 이벤트는 일반적으로 터치 스크린을 가진 디바이스에서 이용가능하다.
하지만, 터치 스크린을 갖는 디바이스를 포함한 모든 데스크탑 디바이스에서 터치 이벤트 API를 사용할 수 없다.
따라서 모바일 환경과 데스크탑 환경을 구분해야 한다.
Next.js 서버 컴포넌트에서 header
의 user-agent
값을 이용해서 모바일인지 확인하고,
클라이언트 컴포넌트에 props
로 isMobile
값을 전달한다.
import { headers } from "next/headers"; const header = headers(); const userAgent = header.get("user-agent"); // 크롬 개발자 도구에서 userAgent : // Iphone 12 Pro -> Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) ... // Galaxy S20 Ultra -> Mozilla/5.0 (Linux; Android 13; SM-G981B) ... export const isMobile = (str: string) => { let mobileRegex = /Android|iPhone|iPad|iPod|BlackBerry|Windows Phone/i; return !!str.match(mobileRegex); };
import { headers } from "next/headers"; const header = headers(); const userAgent = header.get("user-agent"); // 크롬 개발자 도구에서 userAgent : // Iphone 12 Pro -> Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) ... // Galaxy S20 Ultra -> Mozilla/5.0 (Linux; Android 13; SM-G981B) ... export const isMobile = (str: string) => { let mobileRegex = /Android|iPhone|iPad|iPod|BlackBerry|Windows Phone/i; return !!str.match(mobileRegex); };
모바일 환경이면 터치 이벤트 객체를 이용하여 슬라이드를 구현하고,
데스크톱 환경이면 마우스 이벤트 객체를 이용하여 슬라이드를 구현한다.
const getPageX = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => { if (isMobile) { const touchEvent = e as TouchEvent<HTMLDivElement>; return touchEvent.changedTouches[0].pageX; } const mouseEvent = e as MouseEvent<HTMLDivElement>; return mouseEvent.pageX; };
const getPageX = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => { if (isMobile) { const touchEvent = e as TouchEvent<HTMLDivElement>; return touchEvent.changedTouches[0].pageX; } const mouseEvent = e as MouseEvent<HTMLDivElement>; return mouseEvent.pageX; };
터치 이벤트를 기준으로 설명하겠다.
boxWidth
: 현재 브라우저 창의 크기에 대한 박스의 크기
startPageX
: 브라우저 창을 기준으로 한 터치가 시작된 절대적인 위치
curPageX
: 현재 페이지에 대한 터치가 시작된 상대적인 위치
scrollX
: 현재 포인터 위치에 따라 동적으로 스크롤하기 위한 값
isOnScroll
: 현재 스크롤(슬라이드) 중인지를 나타내는 boolean 값
isLongPressScroll
: 꾹 일정 시간이상 터치를 했는지 확인하는 boolean 값
startPageX
: offset으로 페이지 전환하기 위한 상태 변수
curPageX : onTouchMove 이벤트가 발생할 때 scrollX를 변경하기 위한 상태 변수
const onSlideStart = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => { let pageX = getPageX(e); setStartPageX(pageX); setCurPageX(pageX - scrollX); setIsOnScroll(true); startLongPressScrollTimer(); };
const onSlideStart = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => { let pageX = getPageX(e); setStartPageX(pageX); setCurPageX(pageX - scrollX); setIsOnScroll(true); startLongPressScrollTimer(); };
startPageX
와 curPageX
를 설정하고, onScroll
를 true로 변경한다.
꾹 눌렀는지 확인하는 타이머를 시작한다.
const onSlide = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => { if (!isOnScroll) return; let pageX = getPageX(e); if (pageX - curPageX >= 0 || pageX - curPageX <= -boxWidth * (length - 1)) return; setScrollX(pageX - curPageX); };
const onSlide = (e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>) => { if (!isOnScroll) return; let pageX = getPageX(e); if (pageX - curPageX >= 0 || pageX - curPageX <= -boxWidth * (length - 1)) return; setScrollX(pageX - curPageX); };
onTouchMove
의 이벤트 객체의 pageX 값을 이용하여 scrollX
를 설정한다.
첫번째 페이지에서 왼쪽으로 슬라이드 및 마지막 페이지에서 오른쪽으로 슬라이드하는 것을 막는다.
const onSlideEnd = ( e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement> ) => { if (!isOnScroll) return; setIsOnScroll(false); initLongPressScrollTimer(); // 오랫동안 눌렀을 때는 절반 이상 넘어가야 페이지 전환 if (isLongPressScroll) { setScrollX( -boxWidth * (Math.ceil((scrollX - boxWidth / 2) / -boxWidth) - 1) ); setIsLongPressScroll(false); return; } // 짧게 눌렀을 때는 조금만 움직여도 페이지 전환 let pageX = getPageX(e); const offset = pageX - startPageX; if (offset === 0) return; if (offset < -10) { if (Math.ceil(scrollX / -boxWidth) >= length) return; setScrollX(-boxWidth * Math.ceil(scrollX / -boxWidth)); } if (offset > -10) { if (Math.ceil(scrollX / -boxWidth) <= 0) return; setScrollX(-boxWidth * (Math.ceil(scrollX / -boxWidth) - 1)); } };
const onSlideEnd = ( e: TouchEvent<HTMLDivElement> | MouseEvent<HTMLDivElement> ) => { if (!isOnScroll) return; setIsOnScroll(false); initLongPressScrollTimer(); // 오랫동안 눌렀을 때는 절반 이상 넘어가야 페이지 전환 if (isLongPressScroll) { setScrollX( -boxWidth * (Math.ceil((scrollX - boxWidth / 2) / -boxWidth) - 1) ); setIsLongPressScroll(false); return; } // 짧게 눌렀을 때는 조금만 움직여도 페이지 전환 let pageX = getPageX(e); const offset = pageX - startPageX; if (offset === 0) return; if (offset < -10) { if (Math.ceil(scrollX / -boxWidth) >= length) return; setScrollX(-boxWidth * Math.ceil(scrollX / -boxWidth)); } if (offset > -10) { if (Math.ceil(scrollX / -boxWidth) <= 0) return; setScrollX(-boxWidth * (Math.ceil(scrollX / -boxWidth) - 1)); } };
if (isLongPressScroll) { setScrollX( -boxWidth * (Math.ceil((scrollX - boxWidth / 2) / -boxWidth) - 1) ); setIsLongPressScroll(false); return; }
if (isLongPressScroll) { setScrollX( -boxWidth * (Math.ceil((scrollX - boxWidth / 2) / -boxWidth) - 1) ); setIsLongPressScroll(false); return; }
위 코드는 아래 코드를 일반화한 것이다.
// 현재 페이지에서 scrollX가 boxWidth의 절반 이상 넘어가면 다음 페이지로 이동 // scrollX가 boxWidth의 절반 이상 넘어가지 못하면 현재 페이지로 이동 if (isLongPressScroll) { if (scrollX > -boxWidth / 2) { setScrollX(0); } else if (scrollX < -boxWidth / 2 && scrollX > (-boxWidth * 3) / 2) { setScrollX(-boxWidth * 1); } else if (scrollX < (-boxWidth * 3) / 2 && scrollX > (-boxWidth * 5) / 2) { setScrollX(-boxWidth * 2); } else if (scrollX < (-boxWidth * 5) / 2 && scrollX > (-boxWidth * 7) / 2) { setScrollX(-boxWidth * 3); } setIsLongPressScroll(false); return; }
// 현재 페이지에서 scrollX가 boxWidth의 절반 이상 넘어가면 다음 페이지로 이동 // scrollX가 boxWidth의 절반 이상 넘어가지 못하면 현재 페이지로 이동 if (isLongPressScroll) { if (scrollX > -boxWidth / 2) { setScrollX(0); } else if (scrollX < -boxWidth / 2 && scrollX > (-boxWidth * 3) / 2) { setScrollX(-boxWidth * 1); } else if (scrollX < (-boxWidth * 3) / 2 && scrollX > (-boxWidth * 5) / 2) { setScrollX(-boxWidth * 2); } else if (scrollX < (-boxWidth * 5) / 2 && scrollX > (-boxWidth * 7) / 2) { setScrollX(-boxWidth * 3); } setIsLongPressScroll(false); return; }
if (offset < -10) { if (Math.ceil(scrollX / -boxWidth) >= length) return; setScrollX(-boxWidth * Math.ceil(scrollX / -boxWidth)); }
if (offset < -10) { if (Math.ceil(scrollX / -boxWidth) >= length) return; setScrollX(-boxWidth * Math.ceil(scrollX / -boxWidth)); }
위 코드는 아래 코드를 일반화한 것이다.
// 오른쪽으로 슬라이드했을 때, if (offset < -10) { // n번째 페이지이면, n+1 페이지로 이동 if (scrollX > -boxWidth) { setScrollX(-boxWidth * 1); } else if (scrollX > -boxWidth * 2) { setScrollX(-boxWidth * 2); } else if (scrollX > -boxWidth * 3) { setScrollX(-boxWidth * 3); } else { return; } }
// 오른쪽으로 슬라이드했을 때, if (offset < -10) { // n번째 페이지이면, n+1 페이지로 이동 if (scrollX > -boxWidth) { setScrollX(-boxWidth * 1); } else if (scrollX > -boxWidth * 2) { setScrollX(-boxWidth * 2); } else if (scrollX > -boxWidth * 3) { setScrollX(-boxWidth * 3); } else { return; } }
코드를 일반화하면 페이지가 늘어나도 코드를 변경할 필요가 없다는 장점이 있다.
쿠키를 이용하여 Next.js 13 환경에서 다크모드를 구현했다. Next.js의 서버 컴포넌트에서는 브라우저의 API를 사용할 수 없다. 즉, 웹 스토리지를 이용할 수 없기 때문
2024-03-11에서 Edge runtime으로 TTFB 문제를 해결했었는데, Express를 Next.js로 이미그레이션 후에는 Edge runtime을 사용할 수 없어서 다른 방법으로 문제를
2024-03-19로컬 환경에서는 애플리케이션의 TTFB(Time To First Byte)가 70~150ms정도 걸리는데, 배포 환경에서는 TTFB가 느리면 1500ms, 빠르면 400ms정도가
2024-03-12Next.js 14는 다음과 같은 가장 중점으로 둔 릴리스이다. - Turbopack: App & Pages Router에서 5,000번의 테스트 통과 - 로컬 서버 시작이 53%
2024-03-12# Contact : jyw966@naver.com
Copyright © doromo. All Rights Reserved.