메모이제이션 실사용

윤뿔소·2023년 12월 1일
0

제약사, 어드민 상품 리스트 메모이제이션 실사용 보기

API 구조 설명 => 상품 리스트 카테고리 => 메모이제이션 적용 후

회사에서 개발 도중 알고리즘을 써본 경혐이 있어 따로 정리하려고 이 글을 작성했다.
알고리즘을 실사용한 건 얼마 없는데 이번에 이렇게 쓰게 돼서 작성하고싶었다.

배경

먼저 약사 - 제약사 간 플랫폼을 만들고 있다. 그에 따라 약에 관련한 상품들을 Table로 정리하여 만들고 있었고, 이미 만들어놓은 상태이다.

여기서 상품 리스트의 API 불러오는데 응답 후 가공해야되는 데이터들 중 각각의 카테고리들이 있다.

위처럼 말이다. 원래는 저거를 constants 폴더에 따로 정리하여 각 코드를 찾아 name으로 치환해 사용하고 있었는데, 카테고리가 늘어감에 따라 constant하게 정리하기 불편해 졌고, 그에 따라 서버에 데이터를 담아두고 서버에서 가져오는 방식으로 마이그레이션하는 도중이었다.

API 설명

1차 카테고리가 있고, 1차 안에 2차 카테고리가 최소 5개가 있고, 2차 하나 고르면 3차 카테고리가 나오는 형식이다.

이런 식으로 말이다.

그래서 위 쿼리들 중 parent_code가 어떤 카테고리가 나오는지 알려줘 API의 code를 API의 Search Query로 넣어 알려줘야한다.

/commons/codes?code=${parent_code} 이런 식으로 말이다!

문제

여기서 문제는 저 상품 아이템들이 최소 몇십개가 된다는 것이다. 그렇다는 말은 상품 아이템 당 3개 씩의 카테고리가 있다.

그렇다는 말은 1차, 2차, 3차 이렇게 각각 서버에 요청을 보내야한다. 그게 10개 있으니 30번 통신해야하고, 만약 상품 리스트를 50건 씩 본다면 자그마치 150번 통신을 해야한다.
무서워서 다시 통신은 못하지만은 처음 봤을 땐 크롬 네트워크 탭이 그렇게 빨리 움직이는 건 처음 봤다,,

이 문제를 어떻게 해결해야할까 생각 중 문득 메모 알고리즘이 떠올랐다.

메모이제이션

Memoization
동일한 계산을 반복해야 할 경우 한 번 계산한 결과를 메모리에 저장해 두었다가 꺼내 씀으로써 중복 계산을 방지할 수 있게 하는 기법

어원이 Memo에서 온 만큼 Memo를 한 뒤 다시 꺼내 쓰는 알고리즘을 뜻한다.

상품 리스트의 불러온 값들을 보면 중복되는 카테고리들이 좀 많다. 그래서 처음 불러온 카테고리가 있다면 네트워크 호출하고, 저장 후 중복된 요청 값이 있다면 저장된 데이터에서 꺼내오도록 로직을 짜야겠다고 생각했다.
바로 메모이제이션!

실사용

이제 실제로 사용해보자. 여기서 무조건 불러오는 것이 1차 카테고리다. 1차 카테고리는 상품 카테고리라면 무조건 불러와야해서 먼저 불러왔다. parent_codeB5인 데이터도 미리 불데이터 호출을 했다.

담을 그릇 선언

  // 리스트 카테고리 상태 및 로직
  const [cateOfList, setCateOfList] = useState<{
    [productId: string]: string[];
  }>({});
// 공통 코드 가져오기 훅
  const { data: cmnCode1 } = useCmnCode('B5');

각각 productId에 담을 카테고리1,2,3의 코드(code)의 이름(name)들을 할당해줄 생각이다. 또한 1차 카테고리를 불러와 리액트쿼리의 쿼리에 담아주도록 한다.

또 2, 3 코드들만 담아줄 객체도 선언해주자. 위완 다른 점이 여기는 2, 3의 코드만 담을 그릇들이다.

  /// 상품 목록에서 각 상품의 1, 2, 3 카테고리 정보를 효율적으로 가져와 화면에 표시 로직
  // 각 카테고리 코드를 저장할 객체 초기화
  const cmnCode2s: {
    [parentCode1: string]: CmnCodeItem[];
  } = {};
  const cmnCode3s: {
    [parentCode2: string]: CmnCodeItem[];
  } = {};

각각 2차 카테고리는 1차의 코드를 Key(예:B501)로 받고 있고 3차는 2차를 Key로 받고 있다.

메모이제이션을 포함한 담기 로직

가장 핵심적인 부분이다. 조건을 잘 살펴보면 메모이제이션에 더 집중할 수 있다.

  // 컴포넌트가 렌더링될 때마다 실행, *메모이제이션* 이용
  useEffect(() => {
    const fetchData = async () => {
      if (productListData && cmnCode1) {
        // 초기 제품별 카테고리 정보 객체, cateOfList 상태에 조합해 set
        const initialCateOfList: { [productId: string]: string[] } = {};
        // 각각의 제품에 대해 순회
        // TODO: Promise.all + map으로 하니 메모이제이션이 안되다가 for...of 루프로 하니 순차적으로 진행돼 메모이제이션이 적용됐다. 왜이러는 걸까..?
        for (const item of productListData) {
          if (item?.category1) {
            // 2단계 카테고리 코드를 가져오기 위한 조건 확인 및 처리
            if (!cmnCode2s[item?.category1]) {
              // fetch 및 정보 가공
              const response2 = await fetch(
                `${SERVER_URL}/commons/codes?code=${item?.category1}`,
                REQ_OPTIONS,
              );
              const cmnCode2Res: CmnCodeResponse = await response2.json();
              const cmnCode2: CmnCodeItem[] = cmnCode2Res.payload.items.flat();
              // 객체에 Parent Code로 할당
              cmnCode2s[item?.category1] = cmnCode2;
            }
            // 3단계 카테고리 코드를 가져오기 위한 조건 확인 및 처리
            if (item?.category3 && !cmnCode3s[item?.category2]) {
              // fetch 및 정보 가공
              const response3 = await fetch(
                `${SERVER_URL}/commons/codes?code=${item?.category2}`,
                REQ_OPTIONS,
              );
              const cmnCode3Res: CmnCodeResponse = await response3.json();
              const cmnCode3: CmnCodeItem[] = cmnCode3Res?.payload.items.flat();
              // 객체에 Parent Code로 할당
              cmnCode3s[item?.category2] = cmnCode3;
            }

            // 각 카테고리 이름을 가져오기
            const category1Name =
              getNameByCode(cmnCode1, item?.category1) || '';
            const category2Name =
              getNameByCode(cmnCode2s[item?.category1], item?.category2) || '';
            const category3Name =
              getNameByCode(cmnCode3s[item?.category2], item?.category3) || '';
            initialCateOfList[item?.productId] = [
              category1Name,
              category2Name,
              category3Name,
            ];
          }
        }

        setCateOfList(initialCateOfList);
      }
    };
    fetchData();
  }, [data, cmnCode1]);
  1. useEffect: 컴포넌트가 렌더링될 때마다 실행되는 useEffect 훅을 사용함. 이 훅 안에서 fetchData 함수가 호출.

  2. fetchData 함수: 제품 목록 및 1단계 카테고리 코드(cmnCode1)가 존재하는 경우에만 데이터를 가져오는 로직이 포함.
    굳이 함수로 뺀 이유는 fetch를 사용하기에 비동기적으로 처리(async)하기 위해 따로 뺌.

  3. 제품 목록 순회: for...of 루프를 사용하여 제품 목록을 순회하면서 각 제품에 대한 카테고리 정보를 가져옴.
    주석을 보면 알겠지만 처음엔 map을 사용했는데 호출이 한꺼번에 일어나 안됐다,, 고차함수라는 특성때문인가.. 그래서 정통 for문을 사용했더니 됐다.

  4. 2단계 카테고리 코드 가져오기: 해당 2단계 카테고리 코드가 아직 캐시(저장)돼 있지 않은 경우에만 해당 코드를 가져와서 cmnCode2s 객체에 1을 Key로, 불러온 값을 Value로 저장.

  5. 3단계 카테고리 코드 가져오기: 해당 3단계 카테고리 코드가 아직 캐시(저장)돼 있지 않은 경우에만 해당 코드를 가져와서 cmnCode3s 객체에 2을 Key로, Value로 불러온 값을 저장.

  6. 각 카테고리 이름 가져오기: getNameByCode 함수를 사용하여 각 카테고리의 이름을 가져옴.
    getNameByCode 함수는 API 응답 객체에서 codename을 찾는 유틸 함수다.

  7. 초기 카테고리 정보 설정: initialCateOfList 객체에 각 상품에 대한 1, 2, 3 카테고리 이름을 저장.

  8. setCateOfList: 최종적으로 productId를 Key로, Value로 initialCateOfListsetCateOfList 함수를 사용해 상태를 업데이트.

API 호출과 데이터 처리를 비동기로 처리하고, 메모이제이션을 사용하여 중복 호출을 방지하는 등의 최적화 기법을 적용한 코드다.

화면 리렌더링

이제 넣었으니 화면에 넣어주면 이제 끝이다!

{cateOfList[product?.productId]?.map((cate, idx) => (
  <p key={idx}>{cate}</p>
))}

이런 식으로 하면 깔끔하게 나온다!

결과

짤렸지만 50건 씩 보기로 데이터를 불러왔다.

같은 카테고리가 중복됐지만 각각 1개 씩 불러오는 모습이다. 좋다!!!

마무리

나는 간단하게 useState를 사용해서 만들어 봤다. 물론 이 경우도 한계는 존재한다. 아무래도 로컬 상태이다보니 휘발성이 있고, 또한 여러 번 서버에서 호출을 하기 때문에 여타 리스트를 불러오는 API완 달리 서버 비용이 비교적으로 더 발생할 수 있다.
여기서 더 최적화하려면 전용 DB를 만들어 서버 비용을 줄이든가, 로컬스토리지 + 동기화 로직까지 넣어서 한계를 덜 수 있다.

실제로 내가 알고리즘을 사용해 개발하는 경우가 얼마 없다. 물론 for문이나 조건 등을 걸어서 알고리즘들을 사용하긴 하지만 이 경우는 진짜 면접볼 때 봤던 코딩테스트 느낌이 났어서 신기해 작성해봤다. ㅋㅋㅋ

이를 계기로 알고리즘을 왜 배우는지 더욱이 알게 됐고, 복습을 게을리해서는 안되겠다고 생각했다.

profile
코뿔소처럼 저돌적으로

2개의 댓글

comment-user-thumbnail
2023년 12월 8일

저도 오늘 알고리즘에 대해 필요성을 느꼈는데..ㅠㅠ

데이터를 어떻게 관리하느냐가 가 정말 중요한 것 같아요 그런 의미에서 usememo도 큰 역할을 하는 것 같구요! 잘 읽고 갑니당

답글 달기
comment-user-thumbnail
2023년 12월 10일

메모이제이션 개념만 알고 있지 실제로 쓴적없었는데 이렇게라도 볼수 있어서 도움이 되네여 ㅎㅎ

답글 달기