๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

React

React Infinite Scroll (๋ฌดํ•œ ์Šคํฌ๋กค) ๊ตฌํ˜„ํ•˜๊ธฐ

๋ฐ˜์‘ํ˜•

๐Ÿ€ Infinite Scroll ?

infinite scroll์€ ๋ธŒ๋ผ์šฐ์ €์˜ ํ•˜๋‹จ์œผ๋กœ ์Šคํฌ๋กค์„ ํ•˜๋ฉด
์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋– ํ•œ ์•ก์…˜(ํด๋ฆญ)์„ ํ•˜์ง€ ์•Š์•„๋„ ์•Œ์•„์„œ ์ด๋ฏธ์ง€๊ฐ€ ๋‚˜์˜ค๋„๋ก ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ ์ž…๋‹ˆ๋‹ค.  

 

์ด๋Ÿฌํ•œ ๊ฒƒ์ด ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋ ค๋ฉด 

  • ์Šคํฌ๋กค์ด ํ•˜๋‹จ๋ถ€์— ์™”์Œ์„ ๊ฐ์ง€ํ•  ์ˆ˜ ์žˆ์–ด์•ผํ•˜๊ณ ,
  • ๊ฐ์ง€ ํ–ˆ์„ ๋•Œ ๋‹ค์Œ page์˜ ์‚ฌ์ง„์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ž‘์—…(pagination)์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. 

ํฌ๊ฒŒ ๋‘๊ฐ€์ง€ ๋ฐฉ์‹์ด ์žˆ์Šต๋‹ˆ๋‹ค. 

  • (๋œ ํšจ์œจ์ ์ธ) ๊ธฐ์กด ๋ฐฉ์‹
    • ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ œ๊ณตํ•˜๋Š” api์—๋Š” ์Šคํฌ๋กค(event)์„ ํ•  ๋•Œ ์–ด๋–ค ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰๋˜๊ฒŒํ•˜๋Š” addEventListener์™€ ์–ด๋– ํ•œ element์˜ content๊ฐ€ ์ˆ˜์ง์œผ๋กœ ์–ผ๋งˆ๋‚˜ ์Šคํฌ๋กค๋˜์—ˆ๋Š”์ง€๋ฅผ ํ”ฝ์…€ ๋‹จ์œ„์˜ ์ˆ˜๋กœ ๋‚˜ํƒ€๋‚ด์ฃผ๋Š” Element.scrollTop ํ”„๋กœํผํ‹ฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. 
    • ๊ทธ๋ ‡๋‹ค๋ฉด ์Šคํฌ๋กค์ด ์ผ์–ด๋‚  ๋•Œ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜ ์•ˆ์— ์กฐ๊ฑด๋ฌธ์„ ๋„ฃ์–ด์„œ ์Šคํฌ๋กค์˜ ์œ„์น˜๊ฐ€ ํŠน์ •ํ•œ ๊ณณ์— ์žˆ๋‹ค๋ฉด ๋‹ค์Œ page์˜ ์‚ฌ์ง„์„ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด์ฃ . 
    • ์˜ˆ์‹œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.
      document.addEventListener("mousewheel", ()=> console.log(`scrollTop: ${document.children[0].scrollTop}`))โ€‹
      // console.log()๋Œ€์‹ ์— ์Šคํฌ๋กค์ด ์–ด๋””๊นŒ์ง€ ์™”๋Š”์ง€ ํ™•์ธํ•˜๋„๋กํ•˜๊ณ  if๋ฌธ์œผ๋กœ ์กฐ๊ฑด์„ ์ง€์ •ํ•ด ์ฃผ์–ด์„œ ์Šคํฌ๋กค์ด ์–ด๋Š ์œ„์น˜์— ๋„๋‹ฌํ•˜๋ฉด
      // ๋‹ค์Œ ํŽ˜์ด์ง€์˜ ์‚ฌ์ง„์„ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ํ•˜๋Š” http ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ๋˜๋Š” ๊ฒƒ์ด์ฃ .
  • ๊ถŒ์žฅํ•˜๋Š” ๋ฐฉ์‹
    • ์Šคํฌ๋กค์˜ ๋์— ์žˆ๋Š” ํŠน์ • element๋ฅผ ์ฃผ์‹œํ•˜๊ณ  ์žˆ๋‹ค๊ฐ€ ์ด element์˜ ์ง€์ •๋œ ๋งŒํผ์ด ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚˜๋ฉด(view port์— ๋“ค์–ด์™”์„ ๋•Œ) ํŠน์ • ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜๊ฒŒ ํ•˜๋Š” Intersection Observer API์˜ Thresholds๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์•„๋ž˜ ์‚ฌ์ง„์„ ๋ณด๋ฉด ๋Œ€๋†“๊ณ  ํ•ด๋‹น api๊ฐ€ infinite scroll์— ํ•„์š”ํ•จ์„ ์ ์–ด๋†“์€ ๊ฒƒ์„ ์•Œ ์ˆ˜๋„ ์žˆ์ฃ . ์ €๋Š” ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ ์ด ๋ฐฉ์‹์œผ๋กœ ๋ฆฌ์•กํŠธ์—์„œ infinite scroll์„ ๊ตฌํ˜„ํ•ด ๋ณผ๊นŒ ํ•ฉ๋‹ˆ๋‹ค. 

๐Ÿ€ ๋ฆฌ์•กํŠธ์—์„œ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ• ๊นŒ ?

๊ถŒ์žฅํ•˜๋Š” ๋ฐฉ์‹์„ ์„ค๋ช…ํ•˜๋ฉฐ ํŠน์ • element๋ฅผ ์ฃผ์‹œํ•ด์•ผํ•œ๋‹ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค.
์šฐ์„  ๋ชจ๋“  ํƒœ๊ทธ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ์žˆ๋Š” ํƒœ๊ทธ๋ฅผ ๊ฐ์‹œํ•˜๊ฒŒ ํ•˜๋„๋ก ํ•ด๋ด…์‹œ๋‹ค. 

๋…ธ๋ž€ ๋ฐฐ๊ฒฝ์— ์žˆ๋Š” ๋‚˜๋ฌด ์˜์ž๋ฅผ ๊ฐ์‹œํ•˜๊ฒŒ ๋งŒ๋“ค๊ฑฐ๊ณ ,
์ด ๋‚˜๋ฌด์˜์ž๊ฐ€ ํ™”๋ฉด์— ๋ณด์ด๋Š” ์ˆœ๊ฐ„ ๋‹ค์Œ ํŽ˜์ด์ง€์˜ ์‚ฌ์ง„์„ ์š”์ฒญํ•˜๋Š” http ์š”์ฒญ์„ ๋ณด๋‚ผ ๊ฒ๋‹ˆ๋‹ค. 

  • ํ•ด๋‹น element์˜ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์™€์•ผํ•˜๋Š”๋ฐ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ๋Š” getElementById๋ฅผ ํ†ตํ•ด์„œ ํŠน์ • id๊ฐ€ ์žˆ๋Š” element๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์ง€๋งŒ ๋ฆฌ์•กํŠธ์—์„œ๋Š” useRef๋ฅผ ์ด์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
    import React, { useContexet ,useRef } from 'react';
    import { ImageContext } from '../context/ImageContext';
    
    function ImageList() {
    	const elementRef = useRef(null);
        const {images} = useContext(ImageContext);
        
        const imgList = 
        	images.map((image, index) => ( 
                  <Link
                      key={image.key}
                      to={`/images/${image._id}`}
                      ref={index + 1 === myImages.length ? elementRef : undefined}
                  >
                      <img
                          src={`http://localhost:5000/uploads/${image.key}`}
                          alt=""
                      />
                  </Link>
              ))
    	return(
        	<div className="imageList">{imgList}</div>
        )
    }โ€‹
     ์œ„ ์ฝ”๋“œ๋ฅผ ๋ณด์‹œ๋ฉด ref={index + 1 === images.length ? elementRef : undefined}๋ผ๋Š” ์†์„ฑ ์„ค์ •์„ ํ†ตํ•ด ๋งˆ์ง€๋ง‰ ์‚ฌ์ง„์„ ๊ฐ์‹ธ๋Š” Link(a) ํƒœ๊ทธ์—๋งŒ elementRef๋ฅผ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 
  •  
  • ์ž ๊ทธ๋Ÿผ ์–ด๋–ป๊ฒŒ elementRef๋ฅผ ๊ฐ์‹œํ•˜๋ฉด์„œ ํŠน์ • ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”?
    import React, { useContexet, useRef, useEffect } from 'react';
    import { ImageContext } from '../context/ImageContext';
    
    function ImageList() {
    	const elementRef = useRef(null);
        const {images, loadMoreImages} = useContext(ImageContext);
        
        useEffect(() => {
            if (!elementRef.current) return; //์—๋Ÿฌ์ฒ˜๋ฆฌ
            const observer = new IntersectionObserver(([entry]) => {
                if (entry.isIntersecting) loadMoreImages();
            });
            observer.observe(elementRef.current);
            return () => observer.disconnect();
        }, [loadMoreImages]); // loadMoreImages๋Š” ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ํ†ตํ•ด ๋‹ค์Œ ํŽ˜์ด์ง€์˜ ์‚ฌ์ง„์„ ์š”์ฒญํ•˜๋Š” ํ•จ์ˆ˜ ์ž…๋‹ˆ๋‹ค.
        
        const imgList = 
        	images.map((image, index) => ( 
                  <Link
                      key={image.key}
                      to={`/images/${image._id}`}
                      ref={index + 1 === myImages.length ? elementRef : undefined}
                  >
                      <img
                          src={`http://localhost:5000/uploads/${image.key}`}
                          alt=""
                      />
                  </Link>
              ))
    	return(
        	<div className="imageList">{imgList}</div>
        )
    }โ€‹โ€‹
    • useEffect๋ฅผ ํ†ตํ•ด loadMoreImages ํ•จ์ˆ˜๊ฐ€ ๋ฐ”๋€”๋•Œ ๋‹ค์Œ ํŽ˜์ด์ง€์˜ ์‚ฌ์ง„์„ ์š”์ฒญํ•˜๋Š” ํ•จ์ˆ˜์ธ loadMoreImages๊ฐ€ ํ˜ธ์ถœ๋˜๊ณ , ํ˜ธ์ถœํ•œ ํ›„์— ๊ฐ์‹œํ•˜๋Š” element๋ฅผ ์—†์• ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.  
    • const observer = new IntersectionObserver(([entry]) => {
      • new IntersectionObserver๋Š” ์ฝœ๋ฐฑ๊ณผ ์˜ต์…˜์„ ์ธ์ž๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.
      • ์ฝœ๋ฐฑ ์ „๋‹ฌ์ธ์ž์—๋Š” ๊ตฌ์กฐ๋ถ„ํ•ด ํ• ๋‹น์œผ๋กœ entry๋ฅผ ๋ฝ‘์•„์„œ ๋„ฃ์–ด์ฃผ์—ˆ๊ณ 
      • new IntersectionObserver์˜ ์ฝœ๋ฐฑํ•จ์ˆ˜์˜ ์ „๋‹ฌ์ธ์ž๋กœ ์—ฌ๋Ÿฌ๊ฐœ์˜ entry๋“ค์„ ๋„ฃ์–ด์ฃผ๋ฉด ์—ฌ๋Ÿฌ element๋ฅผ ์ฃผ์‹œํ•  ์ˆ˜ ์žˆ์ง€๋งŒ
        ์ €๋Š” ๋งˆ์ง€๋ง‰ ์‚ฌ์ง„์„ ๊ฐ์‹ธ๋Š” Link(a)ํƒœ๊ทธ๋ฅผ ์ฃผ์‹œํ•  ๊ฒƒ์ด๊ธฐ์— ํ•˜๋‚˜๋งŒ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค.
      • (์ฐธ๊ณ ๋กœ ์˜ต์…˜์€ ์„ค์ •ํ•˜์ง€ ์•Š์•˜๊ธฐ์— thresholds๋Š” 0์œผ๋กœ ๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.)
    • if (entry.isIntersecting) loadMoreImages();
      • entry.isIntersecting์€ ํ•ด๋‹น ์š”์†Œ๊ฐ€ ํ™”๋ฉด์— ๋ณด์ด์ž๋งˆ์ž true๊ฐ€ ๋˜๊ณ  ๋‹ค์Œ ํŽ˜์ด์ง€์˜ ์‚ฌ์ง„์„ ์š”์ฒญํ•˜๋Š” loadMoreImages ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ด์ฃผ๋Š” ๊ฒƒ์ด์ฃ .
      • ์ฆ‰ ํ•ด๋‹น ์š”์†Œ๊ฐ€ ํ™”๋ฉด์— ๋ณด์ด์ž๋งˆ์ž new IntersectionObserver์˜ ์ฝœ๋ฐฑํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
    • observer.observe(elementRef.current);
      • ์ฃผ์‹œํ•  element๋ฅผ ๋„ฃ์–ด์ฃผ๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.
    • return () => observer.disconnect();
      • ๊ธฐ์กด์— ์žˆ๋Š” elementRef๋ฅผ ์—†์• ์ฃผ๋Š” ์—ญํ• ์ž…๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•ด์„œ ๊ถŒ์žฅ๋˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋ฌดํ•œ ์Šคํฌ๋กค์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์•˜์Šต๋‹ˆ๋‹ค.    

 

์ฐธ๊ณ ๋กœ ๋งˆ์ง€๋ง‰ ์‚ฌ์ง„์— ๊ฐ€๊ธฐ ์ „ ์กฐ๊ธˆ ๋” ์ผ์ฐ ์‚ฌ์ง„์„ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์‹ถ๋‹ค๋ฉด...

    const imgList = 
    	images.map((image, index) => ( 
              <Link
                  key={image.key}
                  to={`/images/${image._id}`}
                  ref={index + 1 === myImages.length ? elementRef : undefined}
              >
                  <img
                      src={`http://localhost:5000/uploads/${image.key}`}
                      alt=""
                  />
              </Link>
          ))

์œ„ ์ฝ”๋“œ์—์„œ index + 1์„ index + 3, index + 5, index + 10๊ณผ ๊ฐ™์ด ์˜ฌ๋ ค์ฃผ๋ฉด ์กฐ๊ธˆ๋” ๋น ๋ฅธ ๋กœ๋”ฉ์„ ํ†ตํ•ด ์ข‹์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ค„ ์ˆ˜๋„ ์žˆ๋‹ต๋‹ˆ๋‹ค!

๋ฐ˜์‘ํ˜•