ChatGPT를 활용해서 InfiniteScroll 개발한 후기

예리에르·2023년 7월 16일
1

React

목록 보기
16/17
post-thumbnail
post-custom-banner

ChatGPT 써볼까...?

최근 주변에 개발자 친구들을 만나면서 들었던 질문이 있다. 너희도 개발할 때 ChatGPT 사용해? 였다. 첨에 그 질문을 들었을때 반감과 의문이 들었다.

바로 들었던 생각은 아니? 너는 어떨때 사용해? 였다. 막연히 코드는 개발자가 스스로 짜야하며 아직 주니어 레벨인 내가 스스로 사고가 아닌 도구를 사용하여 코드를 작성한다는 거에 거부감이 있었던거 같다.
또한, 짧지만 2년간 개발을 하면서 한개의 함수를 작성하더라고 그 함수의 역할이 거기서 끝나는 것이 아니라 다양한 곳에서 발생시킬 영향을 고려하면서 작성했었다. 하지만 친구가 대답해준 코드를 정리하거나 쿼리를 짤 때 사용해 답에는 더더욱 의문점이 발생했다. 그 함수에 대한 코드는 정리가 되더라도 만약에 정리된 부분이 다른 부분에서 문제를 발생시킬수 있지 않나?

그래서 변명아닌 변명으로 우리 회사는 아직 ChatGPT 유료버전을 지원해주지 않아. 팀장님도 별말 없으시네 라고 말을 했었다.

하지만 최근에 팀 기술 블로그를 전담으로 맡아 개발하면서 도입하고 싶은 기능이 있었다. 그것은 바로 페이지네이션 이었다.

그때 많이 사용하는 것이 Intersection Observer API이다.
(API에 대한 내용은 밑에서 상세하게 다루겠습니다.) 해당 API 를 적용하고자 하면서 답답함 생겼고 이를 해결하기 위해 여러 방면을 노력하다가 ChatGPT를 사용해 볼까??? 라는 생각이 들었다.


Intersection Observer API

크롬 51버전 부터 사용할 수있는 Web API로 2016년 4월 구글 개발자 페이지를 통해 소개되었다. MDN을 통해 API가 필요한 이유를 알아보면 4가지가 있다.

  • 페이지가 스크롤 되는 도중에 발생하는 이미지나 다른 컨텐츠의 지연 로딩.
  • 스크롤 시에, 더 많은 컨텐츠가 로드 및 렌더링되어 사용자가 페이지를 이동하지 않아도 되게 하는 infinite-scroll 을 구현.
  • 광고 수익을 계산하기 위한 용도로 광고의 가시성 보고.
  • 사용자에게 결과가 표시되는 여부에 따라 작업이나 애니메이션을 수행할 지 여부를 결정.

과거에는 intersection을 구현하려면 Element.getBoundingClientRect() 와 같은 메서드를 호출해야해했다. 하지만 위와 같은 메서드는 메인스레드와 같이 실행되기 때문에 성능에 문제를 일으키는 단점이 있었다.

Intersection Observer API는 감시하고자 하는 요소가 viewport에 요청한 만큼 교차될 때 마다 콜백함수를 실행한다. 즉, 요소의 교차를 감지하기 위해 메인스레드를 사용할 필요가 없어지면서 교차 영역 관리를 최적화 할 수 있다.

어려웠던 부분

현재 개발환경과 다른 예제 코드들

검색을 통해 얻게 된 예시 코드들은 크게 2가지의 내용을 담고 있었다.

  1. 컴포넌트 렌더링과 관련있는 hook인 useEffect 함수를 사용
  2. 데이터 패칭에 많이 사용되는 React Query, SWR 관련 예시

두 예시의 공통점을 Next.js 가 functional component로 이루어져 있다라는 것이었다. 하지만 지금까지 작업되었던 코드들은 class component 개발이 되어있었다. 하나의 Web API를 적용하기 위해 전체적인 구조 변경을 해야하는 상황이었다.

회사에서는 데이터를 패칭할 때 fetch 함수를 사용해서 호출하고 있었다. 가끔 강의나 문서를 찾다보면 많은 곳에서 패칭 관련 라이브러리를 사용하는 모습을 보고 이번 프로젝트에 사용해복 싶었다. 마침 React Query에서 무한 스크롤을 위한 useInfiniteQuery 훅을 제공하고 있다는 사실을 알게 되었다. 적용하고 싶었지만 최근의 라이브러리들은 functional component에 최적화 되어 나와서 아쉬움이 컸다.

길지 않은 개발기간으로 팀원들 설득 후 개발까지 하기에는 불가능했기 때문에 개발환경을 바꾸지 않는 선에서 개발하기로 방향을 정하였다.

ChatGPT 사용해보자!

1.MobX 변수와 componentDidUpdate

1-1. 첫 시도

위에서 찾은 예시들을 보게되면 관찰하고자 하는 요소가 나타나서 콜백함수를 실행하기 위해서 특정 변수의 변화를 감지하고 함수를 실행하게 된다. 그때 많이 사용한 hook이 useEffect 였다. 페이지에서 콜백함수를 실행시킬 함수를 관찰하면서 변경하기 위해 MobX를 사용하고 있었다.

지금까지 props를 통해 받아온 값이 다른경우 렌더링을 update한 경우만 사용했던 터라 어떻게 useEffect와 같은 기능을 mobx와 class component에 구현해야할지 고민이많았다.

그래서 ChatGPT에 던진 질문은

2가지의 답변을 얻었다. 두 답변중 내가 원했던 모습은 observer로 랩핑외어 있는 컴포넌트 형태였다. 하지만 특정변수가 변하면 그 함수가 콜백되는 형태의 업데이트가 되는 답변이 아니었다. 🤨

1-2.class component에 변수를 곁들여



autorun이라는 MobX 함수를 알게되었다. 하지만 개발 구조를 InifiniteScroll을 싱행하는 함수를 따로 파일을 만들어 자체 hook을 만들 생각이었다. 그래서 직접적으로 ChatGPT에 Intersection Observer API 사용법을 물어보는 방식으로 변경하였다.

2. class형태에서 Intersection Observer API 사용 어떻게해?

import React, { Component } from 'react';
import { observable } from 'mobx';

class InfiniteScroll {
  @observable threshold = 0.5;
  private observer: IntersectionObserver | null = null;

  constructor(private callback: () => void, threshold = 0.5) {
    this.threshold = threshold;
  }

  observe(element: Element) {
    this.observer = new IntersectionObserver(this.handleIntersection, {
      threshold: this.threshold,
    });
    this.observer.observe(element);
  }

  disconnect() {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  }

  handleIntersection: IntersectionObserverCallback = (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        this.callback();
      }
    });
  };
}

interface InfiniteScrollProps {
  onLoadMore: () => void;
}

class ScrollContainer extends Component<InfiniteScrollProps> {
  private scrollContainerRef = React.createRef<HTMLDivElement>();
  private infiniteScroll: InfiniteScroll | null = null;

  componentDidMount() {
    if (this.scrollContainerRef.current) {
      this.infiniteScroll = new InfiniteScroll(this.props.onLoadMore);
      this.infiniteScroll.observe(this.scrollContainerRef.current);
    }
  }

  componentWillUnmount() {
    if (this.infiniteScroll) {
      this.infiniteScroll.disconnect();
    }
  }

  render() {
    return (
      <div ref={this.scrollContainerRef} style={{ overflowY: 'scroll', maxHeight: '400px' }}>
        {/* 내용 */}
      </div>
    );
  }
}

// 사용 예시
const App = () => {
  const handleLoadMore = () => {
    console.log('Load more data');
    // 추가 데이터 로드 및 원하는 동작 수행
  };

  return <ScrollContainer onLoadMore={handleLoadMore} />;
};

export default App;


좀더 구체적의 기술스펙와 상황에 대한 질문을 더하여 질문을 하였다. 그 결과 기본적이 예시와 적용하는 방법에 대한 정보를 알려주었다. 여기서 좀더 기술을 더해 현재 프레임워크인 Next.js을 더해 질문을 해봤다.

2-2. Next.js에서는 어떻게 사용해?

import { observer } from 'mobx-react';
import { observable } from 'mobx';
import React, { useEffect, useRef } from 'react';

interface InfiniteScrollProps<T> {
  onLoadMore: () => void;
  data: T[];
  isFetching: boolean;
}

@observer
class InfiniteScroll<T> {
  @observable threshold = 0.5;
  private observer: IntersectionObserver | null = null;

  constructor(private callback: () => void, threshold = 0.5) {
    this.threshold = threshold;
  }

  observe(element: Element) {
    this.observer = new IntersectionObserver(this.handleIntersection, {
      threshold: this.threshold,
    });
    this.observer.observe(element);
  }

  disconnect() {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  }

  handleIntersection: IntersectionObserverCallback = (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        this.callback();
      }
    });
  };
}

const InfiniteScrollContainer: React.FC<InfiniteScrollProps<any>> = ({
  onLoadMore,
  data,
  isFetching,
}) => {
  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const infiniteScroll = useRef<InfiniteScroll | null>(null);

  useEffect(() => {
    if (scrollContainerRef.current) {
      infiniteScroll.current = new InfiniteScroll(onLoadMore);
      infiniteScroll.current.observe(scrollContainerRef.current);
    }

    return () => {
      if (infiniteScroll.current) {
        infiniteScroll.current.disconnect();
      }
    };
  }, []);

  return (
    <div ref={scrollContainerRef} style={{ overflowY: 'scroll', maxHeight: '400px' }}>
      {data.map((item, index) => (
        <div key={index}>{/* 렌더링할 아이템 컴포넌트 */}</div>
      ))}
      {isFetching && <div>Loading...</div>}
    </div>
  );
};

export default InfiniteScrollContainer;

비슷한 셩태이지만 적용된 tsx파일은 functional component 형태였다. 그래서 이전 질문을 통해 얻은 예시를 바탕으로 현재 프로젝트 적용해봤다.

실제 적용해보기

1.InfiniteScroll hook 만들기

import { observable} from "mobx";


export default class InfiniteScroll {
    @observable threshold = 0.1;

    private page = 0;
    private observer: IntersectionObserver | null = null;
    constructor(private callback: (page:number) => void, threshold = 0.1) {
        this.threshold = threshold;
    }

    observe(element: Element) {
        this.observer = new IntersectionObserver(this.handleIntersection, {
            threshold: this.threshold,
        });
        this.observer.observe(element);
    }

    disconnect() {
        if (this.observer) {
            this.observer.disconnect();
            this.observer = null;
        }
    }

    handleIntersection: IntersectionObserverCallback = (entries) => {
        if(entries[0].isIntersecting) {
            this.page +=1;
            this.callback(this.page);
        }
    };
}

위의 예시 코드에 약간의 커스텀을 하였다. 기존 뼈대는 그래로에 pagenation에 필요한 페이지 정보를 핸들하고 그 결과를 콜백함수에 보내주는 과정을 추가하였다.

2. 렌더링 함수에 넣기

@observer
class Page extends React.Component<any> {
      private scrollContainerRef = React.createRef<HTMLDivElement>();
      private infiniteScroll: InfiniteScroll | null = null;
      ...
      
      
    componentDidMount() {

        if (this.scrollContainerRef.current) {
            this.requestPostList(0)
            this.infiniteScroll = new InfiniteScroll(this.handleLoadMore, 0.1);
            this.infiniteScroll.observe(this.scrollContainerRef.current);
        }
    };

     
    componentWillUnmount() {
        if (this.infiniteScroll) {
            this.infiniteScroll.disconnect();
        }
    }


    @action
    private handleLoadMore = (page) => {
        this.postLoading = true;
        this.requestPostList(page);
    }
    
    @action
    private requestPostList = async (page: number) => {
        const query = queryGetPagePosts(page, this.pageSizeByWidth);
        const body = {query};
        const res = await requestGetPosts(body);
        const posts = (res ?? []).map(p => ({...p, content: (p?.content ?? "").substring(0, 128)}));
        runInAction(() => {
            this.posts = [...this.posts, ...posts];
            if (this.posts.length < this.pageSizeByWidth * page) {
                if (this.infiniteScroll) {
                    this.infiniteScroll.disconnect();
                }
            }
            if (res) {
                this.postLoading = false;
            }
        })
    };
      
      render() {
        ...
        <div className={`${style.scroll_observer}`} ref={this.scrollContainerRef}></div>
        ...

      }
}

먼저 관찰한 element를 등록한다.(ref)

이후에 페이지가 mount되면 관찰을 시작한다. 콜백함수은 handleLoadMore은 실행될 때 데이터를 요청한다.
reqeustPostList는 서버에 page와 pageSize를 담아 데이터를 요청한다. 여기서 중요한 점은 response받은 페이지 길이가 현재 page와 pageSize 곱보다 적으면 페이지가 끝났다고 생각하면 되기 때문에 관찰을 멈춘다.

실제 적용된 장면


완성!!!🥳

막간의 팀 개발 블로그 홍보 ㅎㅎㅎ 한번씩 구경와주세요!! 👻💝💖 팀 개발 블로그

후기

지금까지 ChatGPT를 사용해서 InifiniteScroll을 적용해본 후기였습니다. 아직 코딩실력과 지식이 부족하지만 그 부족함을 힘으로 ChaptGPT에 도전하고 새로운 지식을 학습할 수 있었다고 생각합니다.

처음에는 인공지능에 대한 막연한 거부감과 불신이 있었습니다. 하지만 직접 써보고 여러질문을 던져보면서 무작정 멀리 하는 것이 아니라 잘 사용하다면 개발작업에 도움이 될수 있겠다고 생각했습니다.

관련 컬럼을 읽다보면 프롬프트의 역할이 중요하다라는 글을 본적이 있습니다. 써보기 전에는 크게 공감이 되지 않았지만 글에는 나타나지 않은 여러 질문을 던져보면서 프롬프트에 따라 인공지능의 능력을 이끌어내어 일을 효율을 높일수도 있겠다고 생각했습니다. 현재 회사에서는 유료구독 서비스를 지원해주지 않아 아쉬움이 있지만 언젠가 지원해준다면 사용해보고 싶습니다!

지금까지 주니어 개발자의 ChatGPT 사용기를 읽어주셔서 감사합니다! 😚💝

profile
궁금한 프론트엔드 개발자의 개발일기😈 ✍️
post-custom-banner

0개의 댓글