목표 : React 에서 무한 스크롤 구현하기 (Infinity Scroll in React)

여정 : scroll Event Listener -> Throttle 로 최적화 -> Intersection Observer API 사용

 

1. 단순 구현

  • 스크롤 이벤트를 참조하여 다음 데이터 API 호출

코드 구현

  • 가정
    • fetchData() 는 API 를 요청하는 함수
    • 예제에서는 console.log('fetch API 요청') 찍히는 걸로 API 요청을 대신함
useEffect(() => {
	const handleScroll = () => {
		const { scrollTop, offsetHeight } = document.documentElement;
		if (window.innerHeight + scrollTop >= offsetHeight) {
			// 스크롤 위치 바닥일때 데이터 페치
			fetchData() // -> API 요청하는 함수
			console.log('fetch API 요청');
		}
	};
	// 화면 진입시 데이터 페치
	fetchData() // -> API 요청하는 함수
	console.log('fetch API 요청');
	window.addEventListener('scroll', handleScroll);
	return () => window.removeEventListener('scroll', handleScroll);
}, []);
  • 개선 포인트
    • documentElement.scrollTop과 documentElement.offsetHeight는 리플로우(Reflow)가 발생하는 참조이므로 API를 과다로 요청하는 경우 발생 가능

 

2. 쓰로틀 (Throttle) 적용

  • 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것
  • 동일 이벤트가 반복적으로 시행되는 경우 이벤트의 실제 반복 주기와 상관없이 임의로 설정한 일정 시간 간격(밀리세컨드)으로 콜백 함수의 실행을 보장

코드 구현

  1. 기본 Throttle 함수(ver1)
function throttle(callback, delay = 300) {
	let waiting = false

	return (...args) => {
		if (waiting) return

		callback(...args)
		waiting = true
		setTimeout(() => {
			waiting = false
		}, delay)
	}
}
  • callback: 실행 대상이 되는 콜백 함수
  • delay : 얼마 간격으로 함수를 실행할 지 결정하며 millisecond 단위, 기본값으로 0.3초를 주었음
  • waiting 상태가 true 인 경우 실행 없음
  • 최초 함수가 호출되었을 때 waiting은 false
  • 이 때 콜백 함수를 실행한 뒤 다시 waiting은 true가 됩니다.
  • waiting이 true가 되었을 때 delay 밀리초 후에는 waiting이 강제로 false가 되고, 다시 콜백 함수가 실행이 됩니다.

1-2. 기본 Throttle 함수(ver2)

function throttle(callback, delay = 3000) {
	let timer;
	return (...args) => {
		if (timer) return;
		timer = setTimeout(() => {
			callback(...args);
			timer = null;
		}, delay);
	};
}
  • timer 를 생성하는 역할
  • 생성해둔 timer 가 있으면, 콜백함수 실행 없이 return
  • timer 가 없으면, timer 를 만듦
  • 만든 timer 는 delay 시간 이후에 콜백 함수를 실행하는 타이머

⇒ 두가지 throttle 함수를 각각 적용해봤을 때 동작에 차이가 발생했음

 

2. handleScroll 함수에 throttle 함수를 적용

useEffect(() => {
	const handleScroll = throttle(() => {
		const { scrollTop, offsetHeight } = document.documentElement;
		if (window.innerHeight + scrollTop >= offsetHeight) {
			// 스크롤 위치 바닥일때 데이터 페치
			console.log('fetch API 요청');
		}
	});
	// 화면 진입시 데이터 페치
	console.log('fetch API 요청');
	window.addEventListener('scroll', handleScroll);
	return () => window.removeEventListener('scroll', handleScroll);
}, []);
  • ver2 throttle 함수의 경우 : 정상 동작
    1. 스크롤 바닥
    2. 3000ms 대기
    3. timer (setTimeout) 생성
    4. callback 함수 실행
    5. timer 해제
    6. 스크롤 위로 갔다가 다시 바닥
    7. 3000ms 대기
  • ver1 throttle 함수의 경우 이상 동작
    • 함수의 코드 순서를 확인해 보자
      • throttle 함수 ver1
      function throttle(callback, delay = 3000) {
      	let wait = false;
      
      	return (...args) => {
      		if (wait) {
      			return;
      		}
      
      		callback(...args);
      		wait = true;
      		setTimeout(() => {
      			wait = false;
      		}, delay);
      	};
      }
      
      • 이상동작 원인
        • 일단 스크롤 이벤트가 생길 때 마다 callback 함수가 실행되는데,
        • 스크롤 이벤트가 트리거 되는 시작점의 스크롤 위치 값을 들고 감
        • if (window.innerHeight + scrollTop >= offsetHeight) {
        • 그래서 스크롤 바닥 조건에 걸리지 않음
        • fetch 함수를 실행하지 않음
  • 화면 예시
    • 한번에 끝까지 스크롤 한 상태
    • callback 함수가 호출됨

  • scrollTop 위치를 “3” 으로 물고 있음
  • fetch 할 조건 (scrollTop이 아래에 위치해 있는가?) 에 해당되지 않아서 fetch 함수가 실행되지 않는다
  • delay 시간 이후에 callback 함수를 실행해야 원하는 scrollTop 값으로 조건체크를 할 수 있음

  • 함수 변형
function throttle(callback, delay = 3000) {
		let wait = false;

		return (...args) => {
			if (wait) {
				return;
			}
			wait = true;
			setTimeout(() => {
				callback(...args);
				wait = false;
			}, delay);
		};
	}

  • delay 시간 이후 callback 함수가 호출 되어서 최신 scrollTop 위치값을 가져올 수 있

 

⇒ 스크롤 이벤트 특성상 delay 이후에 콜백함수가 실행되는 것이 정상 동작이었으나, 케이스에 따라 콜백을 먼저 실행하고, delay 가 있어야 하는 경우도 있을 것

 

3. Intersection Observer API (체신 문법 적용^^)

  • Intersection Observer API 적용
  • 교차 관찰자 API
  • 브라우저에서 제공하는 API
  • 크롬 51버전부터 사용가능
  • 위에서 확인했듯이, 스크롤 이벤트는 일단 스크롤이 발생하면 트리거됨
  • 우리가 원하는 것은 스크롤이 아래에 도달했을 때만 트리거 시키고 싶음

코드 구현

  • new IntersectionObserver() 생성자 표현식을 사용하여, observer 객체를 생성하여 사용
  1. 생성
    • 교차될때 실행할 콜백함수를 제공해야 한다 → fetchData 함수
    • options 로 root / rootMargin / threshold 값을 커스텀 할 수 있음
  2. 설정
    • options 값 커스텀
      • root : 대상이 되는 객체의 가시성을 확인 할 때 사용하는 뷰포트 요소, 기본값은 브라우저 뷰포트

      • rootMargin : root 가 가진 여백, 교차성을 개산하기 전에 적용됨

      • threshold : observer의 콜백함수가 실행될 대상 요소의 가시성 퍼센테이지, 10%만 보여도 수행하는가?, 100% 다보였을때 실행하는가

 

3. 관찰대상 지정하기

  • 요소를 observer.observe 메소드에 등록해주면 된다
let target = document.querySelector('#listItem'); 
observer.observe(target);

4. 콜백함수 구현하기

  • 콜백함수 기본 args
let callback = (entries, observer) => {
  entries.forEach(entry => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};
  • IntersectonObserverEntry 리스트를 첫번째 인자로 받고, observer 자체를 두번째 인자로 받는다
  • React 용 useIntersect hook
const { useRef, useCallback, useEffect } = require('react');

const useIntersect = (onIntersectFn, options) => {
	const ref = useRef(null); // 1
	const callback = useCallback(
		(entries, observer) => {
			entries.forEach((entry) => {
				if (entry.isIntersecting) onIntersectFn(entry, observer);
			});
		},
		[onIntersectFn]
	);

	useEffect(() => {
		if (!ref.current) return;
		const observer = new IntersectionObserver(callback, options); // 2
		observer.observe(ref.current); // 3
		return () => observer.disconnect();
	}, [ref, options, callback]);

	return ref;
};

export default useIntersect;
  1. 관찰 타겟 ref 생성
  2. observer 생성
  3. observer.observe 에 관찰 타겟 ref 연결
  4. ref unmount 시 observer disconnect 시키기 (삭제)
  5. callback 함수 선언
    • entry 객체 내부 key

    • entry 가 isIntersecting true 인터섹트 트리거 되면,
    • onIntersectFn 함수를 실행
    • onIntersectFn은 useIntersect 훅 선언시 전달 → fetchData 함수
  • useIntersect 훅 사용부분
    • 핵심 내용은 fetchData 함수를 실행해야함
    • 타겟에 대한 관찰을 멈출지는 상황에 따라 판단
const ref = useIntersect(async (entry, observer) => {
	// observer.unobserve(entry.target);
	// if(hasNextPage && !isFetching) {
	// 	console.log('fetchNextPage');
	// }
	console.log('fetchNextPage');
});

 

4. 폴리필

  • safari 에서 지원하고 있지 않음 폴리필 필요

https://github.com/GoogleChromeLabs/intersection-observer

https://github.com/w3c/IntersectionObserver

$ npm i intersection-observer
import 'intersection-observer'

const io = new IntersectionObserver(콜백함수, 옵션)

const 관찰대상 = document.querySelectorAll(관찰대상)

// IE에서 사용하기
Array.prototype.slice.call(관찰대상).forEach(elim => {
  io.observe(elim)
})

 

참고

 

 

Warning: Prop `className` did not match. Server: "sc-gKclnd ctBXSH" Client: "sc-dkPtRN hSjZFd"

 

Next.js로 Styled-components 사용하면, 최초 빌드하여 랜더링은 문제 없이 잘 되는데, 몇번 새로고침을 하면 위와같은 에러가 발생할 때가 있다.

 

Next는 처음 페이지는 SSR(서버사이드 랜더링)으로 작동하고 이후는 CSR(클라이언트 사이드 렌더링)으로 작용하기 때문에, 서버에서 받은 해시 클래스명과 이후 클라이언트에서 작동하는 해시클래스명이 달라져서 스타일을 적용하지 못하는 현상이다.

 

[최초 랜더링] : 정상 작동

 

[n번 새로고침 시] : Warning과 함께 스타일 적용 안됨

 

해결방안

바벨 플러그인을 설치하고 바벨 설정을 해주면 해결 할 수 있다.

이 플러그인은 서버와 클라이언트의 클래스명을 일치 시켜주는 역할을 한다.

 

1. 바벨 플러그인 설치

npm i babel-plugin-styled-components

 

2. 프로젝트 루트 파일에 .bablerc 파일 신규 생성

 

3. .babelrc 파일 설정

{
    "presets": ["next/babel"],
    "plugins": [
      [
        "babel-plugin-styled-components",
        { "fileName": true, "displayName": true, "pure": true }
      ]
    ]
  }

* 인덱스 옵션 설명

 { "fileName": true, "displayName": true, "pure": true }
  • fileName: 코드가 포함된 파일명을 알려줌
  • displayName : 클래스명에 해당 스타일 정보 추가
  • pure : 사용하지 않은 속성 제거

 

 

참고링크 : https://velog.io/@hwang-eunji/Styled-components-nextjs%EC%97%90%EC%84%9C-className-%EC%98%A4%EB%A5%98

 

Styled-components # nextjs에서 className 오류

Warning : Props 'className' did not match next로 styled-components로 스타일 적용하고, 개발 서버를 띄워서 확인해보면 첫 페이지 로딩은 문제없이 잘 작동하고, 새로고침 이후 Warning : Props 'classNa

velog.io

 

개발한 웹페이지를 모바일에서 사용하기 위해 여러가지 설정들을 해주어야 한다.

 

그 여러가지 설정 중에 하나를 오늘 기록하려 한다.

 

바로 '홈 화면에 추가' 기능이다.

 

PC라고 생각했을 때 바로가기 기능과 같다.

 

다만 모바일에서 '홈 화면에 추가'를 해두면, 웹이 앱처럼 동작하게 만들어 준다.

 


 

우선 아무 설정을 하지 않았을 때는 React App이라는 Default 설정 값으로 바로가기 이름이 생성된다.

 

 

나는 이 React App을 서비스 이름으로 변경하고 싶었다.


 

홈화면에 추가 (바로가기 기능)에서 참조하는 파일이 바로 

 

manifest.json 파일이다.

 

최초 리액트 세팅시 create-react-app 명령어를 통해서 public 폴더에 manifest.json 파일이 생성된다.

 

manifest.json 

웹앱 매니패스트 파일은 앱에 대한 정보를 담고 있는 json 파일이다. 

 

{
  "short_name": "TrackDay",
  "name": "Create React App Sample",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    },
    {
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "logo512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

 

[Key 별 설정 내용]

  • short_name : 사용자 홈 화면에서 아이콘 이름으로 사용
  • name : 웹앱 설치 배너에 사용
  • icons : 홈 화면에 추가할때 사용할 이미지
  • start_url : 웹앱 실행시 시작되는 URL 주소
  • display : 디스플레이 유형(fullscreen, standalone, browser 중 설정)
  • theme_color : 상단 툴바의 색상
  • background_color : 스플래시 화면 배경 색상
  • orientation : 특정 방향을 강제로 지정(landscape, portrait 중 설정)

 

-> 결론적으로 맨 첫번째 줄의 short_name 값을 원하는 서비스 명으로 변경해주면 바로 완성이다.

 

다음과 같이 서비스명이 자동으로 변경 되었다

 

+ Recent posts