• 2021. 10. 27.

    by. 문익점

    반응형

    React를 사용 하다보면 컴포넌트 안 에서 데이터를 패칭해서 가져와야 할 때가 있습니다. 그 경우 컴포넌트안에 마다 axios를 호출하고 baseUrl를 매번 입력한다면 같은 코드가 반복되고 생산성이 떨어지게 됩니다. 이 경우 앞선 포스팅을 참고하면 Asyncboundary를 사용하면 데이터를 가져오는 작업을 더욱 수월하게 할 수 있습니다. 하지만 여기서 문제점은 가져온 데이터 data를 어떻게 관리 할지 입니다. 아래 코드를 봅시다.

    function Example() {
        let [data, setPhotos] = useState([]);
    
        function searchApi() {
            const url = "https://jsonplaceholder.typicode.com/photos";
            axios.get(url)
            .then(function(response) {
                setPhotos(response.data);
                console.log("성공");
            })
            .catch(function(error) {
                console.log("실패");
            })
        }
    
        useEffect(()=> {
            searchApi();
        },[])
    
    }
    export default Example;

    서버로 부턴 받은 데이터를 그대로 저장하고 있습니다. 이렇게 된다면 효율적인 데이터 관리가 불가능 하게 됩니다. 그 이유는 우선 개선을 하고나서 차이점을 통해 알아보도록 합니다.

    Axios Class로 분리하기

    매번 axios config 설정을 해야되는 상황이 생기게 되고 이는 코드 중복을 일으키게 됩니다. 이를 방지하기 위해서 공통되는 config를 설정한 axios 인스턴스를 제공해주는 클래스를 하나 만들어 제공합니다.

    import axios from "axios";
    
    class AxiosInstance {
      private static DEFAULT_URL = "https://httpbin.org";
      private static TIME_OUT = 3000;
    
      static createInstance(baseUrl?: string) {
        return axios.create({
          baseURL: baseUrl ? baseUrl : AxiosInstance.DEFAULT_URL,
          timeout: AxiosInstance.TIME_OUT,
        });
      }
    }
    
    export default AxiosInstance;

    아주 간단하게 baseUrl과 timeout 설정을 한 axios 인스턴스를 만드는 클래스입니다. 이는 axios를 사용하는 다른 곳에서 사용 할 때 가져오기만 하면 됩니다. 예를 들어 음악 검색 목록을 가져오는 api 클래스가 있다고 하겠습니다. 아래는 단순한 예제입니다.

    • 엔드포인트 별로 클래스를 만듭니다.
    • recoil async selector와 함께 쓰입니다.
    • 컴포넌트에서 에러 처리와 패딩 처리는 외부로 위임 합니다. (AyncBoundary 사용)
    class MusicService {
      static search(keyword: string) {
        return AxiosInstance.createInstance().get<MusicReleaseEntity>(
          `/?song=${keyword}`
        );
      }
    }

    일단 MusicService를 구현했습니다. 이 클래스는 음악 관련 api를 모아놓은 클래스입니다. 메소드를 만들 때에 AxiosInstance.createInstance()으로 인스턴스를 가져온 뒤에 사용 할 http 메소드를 호출하면 됩니다.

    Rcoil 비동기 셀랙터와 데이터 클래스

    이 메소드는 recoil async selector에서 데이터를 받아오게 처리 해보겠습니다. 먼저 데이터를 관리 할 클래스와 비동기 셀렉터를 구현한 예제를 입니다.

    export interface MusicEntity {
      name: string;
      artist: string;
      album: string;
      albumimg: string;
      date: string;
      melonlink: string;
      kakaomelonlink: string;
      lyrics: string;
    }
    
    export interface MusicReleaseEntity {
      type: string;
      status: string;
      lineup: string[];
      song: MusicEntity[];
    }
    
    export class Music {
      private name: string;
      private artist: string;
      private album: string;
      private albumimg: string;
      private date: string;
      private melonlink: string;
      private kakaomelonlink: string;
      private lyrics: string;
    
      constructor(entity: MusicEntity) {
       ... 생략
      }
    
      public checkNewRelease(date: string) {
          this.date ....
        ... 최신곡인지 체크하는 로직
      }
    }
    
    export class MusicList {
      list: Music[];
      constructor(list: Music[]) {
        this.list = list;
      }
    }
    
    const musicSelector = selectorFamily<MusicList, string>({
      key: "musicState",
      get:
        (keyword) =>
        async ({ get }) => {
          const response = await MusicService.search(keyword);
          const list = response.data.song.reduce<Music[]>(
            (acc, song) => (acc = [...acc, new Music(song)]),
            []
          );
          return new MusicList(list);
        },
    });
    
    export function useSearchMusicList(keyword: string) {
      return useRecoilValue(musicSelector(keyword));
    }

     

    musicSelector의 비동기 셀럭터를 이용하여 데이터를 받아오고 그에 맞는 클래스로 객체를 생성하여 데이터만 리턴합니다. 중간에 Music 객체로 맵을 돌려 MusicList라는 객체를 리턴하게 해서 이 비동기 셀렉터를 이용하는 컴포넌트 내부에서 더욱 쉽게 데이터를 관리 할 수 있게 됩니다. (이는 예시일 뿐이며 MusicList를 만들지 않고 바로 Music 배열을 데이터로 받도록해도 됩니다!)

    바로 예를 들어 아래 비동기 셀렉터로 데이터를 받는 컴포넌트 코드를 보면,

    function MusicList({ keyword }: MusicListProps) {
      const musicList = useSearchMusicList(keyword);
    
      return (
        <ul>
          {musicList.list.map((song, i) => (
            <li key={`song-${i}`}>
              {song.name} - {song.artist} {song.checkNewRelease(date) ? "-new" : ""}
            </li>
          ))}
        </ul>
      );
    }
    
    export default MusicList;

    useSearchMusicList 비동기 셀렉터를 사용하는 컴포넌트를 보았을 때 musicList를 쉽게 가지고 와서 음악 리스트를 랜더링 해줄 수 있습니다. 또한 Music 클래스안에는 checkNewRelease() 라는 메소드가 정의되어 있어 쉽게 신곡인지 아닌지 판단하는 처리를 할 수 있습니다. 즉 이전과 차이점은,

    이전 → music 데이터에 date 값을 현재와 비교해서 신곡인지 아닌지 판단하는 함수를 컴포넌트 내부에 정의해야 함. 즉 API 통신으로 받은 데이터의 정보를 참조하며 최신 곡인지 비교하는 함수를 컴포넌트 내부에 만들어야 함(또는 커스텀 훅 분리) -> 컴포넌트 비지니스 로직을 파악하는데 방해하는 요소.

    개선 → Music이라는 객체에 내부 메소드 checkNewRelease()으로 쉽게 처리가 가능함. Music이 신곡인지를 판단하는 함수는 Music 객체가 가지고 있고 MusicList 컴포넌트는 리스트를 보여주는 로직에 충실할 수 있다!(캡슐화!)

    캡슐화: https://varletc0nst.tistory.com/19?category=924743

    정리

    Axios를 따로 클래스로 분리하고 비동기 처리 이후 받는 데이터도 클래스 객체로 받게하므로써 컴포넌트 내부에서 좀 더 편리하고 쉽게 데이터 처리가 가능합니다. 또한 컴포넌트가 가진 비지니스 로직을 더 쉽게 파악 할 수 있게 됩니다.

    반응형