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

๊ฐœ๋ฐœ

React + Express ๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ

ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋ฉด์„œ ๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ด๋ณด์•˜๋‹ค. ํ”„๋ก ํŠธ์—์„œ๋„ ์ฒ˜์Œ ๊ตฌํ˜„ํ•˜๋Š” ๊ธฐ๋Šฅ์ด์—ˆ์ง€๋งŒ ๋ฐฑ์—”๋“œ๋„ ์ฒ˜์Œ์ด๋ผ ์—ฌ๋Ÿฌ ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๊ฒช์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๊ทธ ๊ณผ์ •์„ ๊ฐ„๋žตํžˆ ๊ธฐ๋กํ•˜๊ณ ์ž ํ•œ๋‹ค. 


#. ๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ

๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๋ฒˆ์— ๋ถˆ๋Ÿฌ์˜ค๊ฒŒ ๋˜๋ฉด ๊ทธ๋งŒํผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋กœ๋”ฉ๋˜์–ด ๋ณด์—ฌ์ง€๋Š” ์†๋„๊ฐ€ ๋Šฆ์–ด์ง€๊ฒŒ ๋œ๋‹ค. ๋”ฐ๋ผ์„œ ์ฒ˜์Œ ๋กœ๋”ฉ๋˜์—ˆ์„ ๋•Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ผ์ • ๋ฐ์ดํ„ฐ๋งŒ ๋จผ์ € ์ œ๊ณตํ•˜๊ณ  ์ดํ›„ ์‚ฌ์šฉ์ž๊ฐ€ ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋ ค๊ณ  ์Šคํฌ๋กค์„ ์›€์ง์ด๋Š” ์•ก์…˜์„ ์ทจํ•˜๋ฉด, ๊ทธ๋•Œ ์ถ”๊ฐ€๋กœ ๋˜ ์ผ์ • ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ด์ฃผ๋Š” ๊ฒƒ์„ ๋ฐ˜๋ณตํ•˜๋Š” ๊ฒƒ์ด ๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ์ด๋‹ค.

 

 

#. ๋ฐฑ์—”๋“œ ๊ตฌํ˜„

๋ชฉํ‘œ: ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฐ์ดํ„ฐ ์š”์ฒญ ์‹œ ์•Œ๋งž๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋„˜๊ฒจ์ฃผ๋Š” ๊ฒƒ!

๋ฐฑ์—”๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ ๊ฐ€์žฅ ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๊ฒช์—ˆ๋‹ค. ๋ฐฑ์—”๋“œ์—์„œ ์–ด๋–ป๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ด์ฃผ๋Š”๋ƒ์— ๋”ฐ๋ผ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋Š” ๋ฐฉ์‹๋„ ๋‹ฌ๋ผ์ง€๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ๋‹ฌ์•„ ์—ฌ๋Ÿฌ๋ฒˆ ์ˆ˜์ •์ด ์ด๋ฃจ์–ด์กŒ๋‹ค.

์ฒ˜์Œ์—” ๋ฐฑ์—”๋“œ๋ฅผ ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜์œผ๋กœ ๋™์ž‘ํ•˜๊ฒŒ๋” ์ฝ”๋“œ๋ฅผ ์งฐ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๋ฐฉ์‹์ด ๋‹ค๋ฅผ ๋ฟ ์„œ๋ฒ„์—์„  ๋ฌดํ•œ ์Šคํฌ๋กค์ด๋“  ํŽ˜์ด์ง€๋„ค์ด์…˜์ด๋“  ๋˜‘๊ฐ™์ด ์ถ”๊ฐ€๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์•„ ์ „๋‹ฌํ•ด์ฃผ๋ฉด ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ทธ๋ž˜์„œ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ฒซ ๋žœ๋”๋ง ๋˜์–ด์ง€๋Š” ๋ฐ์ดํ„ฐ์˜ ๋งˆ์ง€๋ง‰ ๋ฒˆํ˜ธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ cursorId๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜์˜€๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์นœ๊ตฌ์˜ ํ”ผ๋“œ๋ฐฑ์œผ๋กœ ์ด๋Ÿฐ ๋ฐฉ์‹๋ณด๋‹จ ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์‹œ ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋Š” api ์ฃผ์†Œ๋ฅผ ๊ฐ™์ด ์ „๋‹ฌํ•˜๋Š”๊ฒŒ ๋” ์ข‹์„๊ฑฐ ๊ฐ™๋‹ค๋Š” ๋ง์„ ๋“ค์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ๋ง์„ ๋“ฃ๊ณ  ๋ณด๋‹ˆ ์ƒˆ์‚ผ YouTube API๋„ ์ด๋Ÿฌํ•œ ๊ตฌ์กฐ์ž„์„ ๋– ์˜ฌ๋ ธ๋‹ค.

YouTube API๋„ ํ•œ๋ฒˆ์— ์ตœ๋Œ€ 50๊ฐœ์”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ณ  ๊ทธ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์œผ๋ ค๋ฉด ์‘๋‹ต ์‹œ ๋ฐ›์€ nextPageToken์„ ํ•จ๊ป˜ ๋ณด๋‚ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ์œ ํŠœ๋ธŒ๋„ ๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ์ด ์‚ฌ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์นœ๊ตฌ์˜ ํ”ผ๋“œ๋ฐฑ์ด ๋” ์ ์ ˆํ•œ ๋ฐฉ๋ฒ•์ด๊ตฌ๋‚˜ ์‹ถ์–ด ์ „๋ฉด ์ˆ˜์ •ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. ์ˆ˜์ •๋œ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. 

 

// controller code

export const latestVideosByPage = async (req, res) => {
  const { pageNum } = req.query;
  const data = await getLatestVideosByPage(pageNum);
  let nextPage =
    data.length === 0 ? "" : `videos/latest?pageNum=${parseInt(pageNum) + 1}`;

  return res.json({ data, nextPage });
};


// model code

export const getLatestVideosByPage = async (pageNum) => {
  const SELECT_VIDEO = `select etag, id, snippet from latestVideos where idx > ${
    (pageNum - 1) * 15
  } limit 15;`;
  const [row, field] = await connection.promise().query(SELECT_VIDEO);
  return row;
};


์œ„ ์ฝ”๋“œ๋ฅผ ๊ฐ„๋žตํžˆ ์„ค๋ช…ํ•˜์ž๋ฉด, ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์‹œ nextPage ๊ฐ’์œผ๋กœ 'http://localhost:5000/videos/latest?pageNum=2'๋ฅผ ์ „๋‹ฌํ•œ๋‹ค. ๊ทธ๋Ÿผ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋‹ค์Œ ๋ฐ์ดํ„ฐ ์š”์ฒญ ์‹œ ํ•ด๋‹น url๋กœ ์„œ๋ฒ„์— ์š”์ฒญํ•˜๊ณ , ์„œ๋ฒ„์—์„  query๋กœ pageNum ๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ ๋‹ค์Œ ํŽ˜์ด์ง€์— ๋งž๋Š” ๋ฐ์ดํ„ฐ๋ฅผ DB์—์„œ ์ฐพ์•„์˜จ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ์ „๋‹ฌํ•  ๋•Œ ๋˜ ๋‹ค์Œ ํŽ˜์ด์ง€ ์š”์ฒญ url์ธ 'http://localhost:5000/videos/latest?pageNum=3'์„ ํ•จ๊ป˜ ์ „๋‹ฌํ•˜๋Š” ๊ตฌ์กฐ์ด๋‹ค. ๋!
(** ์ฐธ๊ณ ๋กœ ๋‚˜๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋ฅผ ์ค€๋น„์ค‘์ด๋ผ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ ์ˆ˜์ค€์ด ๋ฏธํกํ•  ์ˆ˜ ์žˆ๋‹ค ๐Ÿ™)

 


#. ํ”„๋ก ํŠธ์—”๋“œ ๊ตฌํ˜„

๋ชฉํ‘œ: ์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด์„ ์Šคํฌ๋กคํ•ด์„œ ํŠน์ • ์œ„์น˜๊ฐ€ ๋‹ฟ์œผ๋ฉด ์„œ๋ฒ„์—๊ฒŒ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€๋กœ ์š”์ฒญํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ!

๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด ๊ตฌ๊ธ€๋ง ํ•ด๋ณด๋‹ˆ ํฌ๊ฒŒ ๋‘๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์—ˆ๋‹ค. ์ฒซ๋ฒˆ์งธ ๋ฐฉ๋ฒ•์€ window์— ์Šคํฌ๋กค ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด ํ™”๋ฉด ์Šคํฌ๋กค์ด ๋งจํ•˜๋‹จ์— ๋‹ฟ์•˜์„ ๋•Œ๋ฅผ ๊ฐ์ง€ํ•˜๋Š” ๊ฒƒ. ๋‘๋ฒˆ์งธ ๋ฐฉ๋ฒ•์€ react-intersection-observer๋ฅผ ์ด์šฉํ•ด ๋งจํ•˜๋‹จ์˜ ํŠน์ • ํƒœ๊ทธ๊ฐ€ ๋ณด์—ฌ์งˆ ๋•Œ๋ฅผ ๊ฐ์ง€ํ•˜๋Š” ๊ฒƒ.

 

์ฒ˜์Œ์—” ๋‘๋ฒˆ์งธ ๋ฐฉ๋ฒ•์ธ react-intersection-observer์„ ์ด์šฉํ•ด ๊ตฌํ˜„ํ–ˆ๋‹ค. react-intersection-observer๋ฅผ ์„ค์น˜ํ•˜๊ณ  ํŽ˜์ด์ง€ ๋งจ ํ•˜๋‹จ์— <span> ํƒœ๊ทธ๋ฅผ ๋งŒ๋“ค์–ด ๊ฑฐ๊ธฐ์— ref ์†์„ฑ์„ ์ง€์ •ํ•ด๋‘์—ˆ๋‹ค. ๊ทธ๋Ÿผ ํ™”๋ฉด์„ ์Šคํฌ๋กคํ•ด์„œ ํ•ด๋‹น ํƒœ๊ทธ๊ฐ€ ๋ณด์—ฌ์งˆ ๋•Œ๋งˆ๋‹ค inview ๊ฐ’์ด false์—์„œ true๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ๊ฐ’์ด true๊ฐ€ ๋  ๋•Œ ์„œ๋ฒ„์— ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋Š” api๋ฅผ ํ˜ธ์ถœํ–ˆ๋‹ค. (*ref, inview๋Š” react-intersection-observer์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ)

๊ทธ๋Ÿฐ๋ฐ ์ด์™•์ด๋ฉด ๋‹ค๋ฅธ ํŒจํ‚ค์ง€ ์‚ฌ์šฉ ์—†์ด ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์„๊ฑฐ ๊ฐ™์•„ ๋‹ค์‹œ ์ฒซ๋ฒˆ์งธ ๋ฐฉ๋ฒ•์œผ๋กœ ์ˆ˜์ •ํ•˜์˜€๋‹ค. ๊ทธ๋ž˜์„œ useEffect๋ฅผ ์ด์šฉํ•ด window์˜ ์Šคํฌ๋กค ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ํ•˜์˜€๊ณ , ์Šคํฌ๋กค์ด ์›ํ•˜๋Š” ์œ„์น˜๊ฐ€ ๋˜์—ˆ์„ ๋•Œ isHitBottom์ด๋ž€ ๊ฐ’์„ false์—์„œ true๋กœ ๋ณ€๊ฒฝ๋˜๋„๋ก ํ–ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ด๋•Œ ์ด๋ฒคํŠธ๋ฅผ ์‹คํ–‰ํ•œ ๋‹ค์Œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ”๋กœ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค. ๋งŒ์•ฝ ์ œ๊ฑฐํ•˜์ง€ ์•Š์œผ๋จ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋žœ๋”๋ง๋  ๋•Œ๋งˆ๋‹ค window์— ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๊ณ„์† ์ถ”๊ฐ€ํ•˜๊ฒŒ ๋˜์–ด ์˜๋™์น˜ ์•Š์€ ๋™์ž‘์ด ๋ฐœ์ƒํ•˜๊ฑฐ๋‚˜ ๋ฉ”๋ชจ๋ฆฌ์ ์œผ๋กœ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๊ธฐ๋„ ํ•œ๋‹ค(๊ทธ๋ ‡๋‹ค๊ณ  ํ•œ๋‹ค๐Ÿ™‚)

๋”ฐ๋ผ์„œ isHitBottom ๊ฐ’์ด true๊ฐ€ ๋  ๋•Œ ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•˜๋Š” api๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด๋œ๋‹ค. nextPageNum์€ ์„œ๋ฒ„์—์„œ ์ด์ „ ๋ฐ์ดํ„ฐ ์‘๋‹ต ์‹œ ๋ณด๋‚ด์ค€ ๋‹ค์Œ ํŽ˜์ด์ง€์˜ api url์— ๋Œ€ํ•œ ๊ฐ’์ด๋‹ค. (*๋น„๋™๊ธฐ ํ˜ธ์ถœ์€ redux-saga๋ฅผ ์ด์šฉํ•จ)

 

// component code

const [isHitBottom, setIsHitBottom] = useState(false);

useEffect(() => {
  const handleScroll = () => {
	const { scroolTop, offsetHeight } = document.documentElement;
	if(window.innerHeight + scrollTop >= offsetHeight) {
      setIsHitBottom(true);
    }
  };

  setIsHitBottom(false);
  window.addEventListener('scroll', hadleScroll);
  return () => window.removeEventListener('scroll', handleScroll);

})

useEffect(() => {
  if (isHitBottom && nextPageNum.length !== 0) {
    dispatch(latestVideosRequest(nextPageNum));
  }
}, [isHitBottom]);



// api code

const LATEST_VIDEOS = "/videos/latest";

export const latestVideosByPage = async (pageNum) => {
  try {
    const res = await axios.get(`${LATEST_VIDEOS}?pageNum=${pageNum}`);
    if (res) return res.data;
  } catch (err) {
    console.log(err);
  }
};