본문 바로가기

프로젝트/푸드피커

[푸드피커 프로젝트] 프로젝트 간략 소개 및 트러블 슈팅 정리

반응형

참고사항 | 프로젝트 회고

원래 해당 페이지에 회고 느낌의 긴 글이 있었는데, 이를 다른 포스트로 분리하였습니다. 마치 목적에 따른 컴포넌트를 분리하는 느낌입니다.

 

[푸드피커] 재개발 후 써보는 프로젝트 회고

들어가기 전, 잡담더 늦기 전에 간단하게라도 프로젝트에 대한 회고를 적어볼까 하여 이렇게 글을 적어봅니다. 들어가기 전에 몸풀기 느낌으로 잡담을 적어볼까 합니다.잡담 첫 번째, 푸드피커

duklook.tistory.com


들어가기 전

프로젝트 재개발을 들어가기 전과 후에 경험했던 문제들을 정리한 포스트 입니다. 사소한 문제로 보일지라도 당시에는 처음 직면하고, 시간이 걸렸던 문제들을 보관해 둡니다.

 


 

[캐싱 문제] 레시피 조회 세부 페이지의 새로고침 시 흰 화면이 렌더링 되는 문제

문제상황-

레시피 데이터의 경우 공공 데이터 포털의 api 를 사용하여 외부 백엔드에서 데이터를 GET 요청으로 받아오고 있습니다. 1일 트래픽의 한도는 1000 회가 한도이므로 세부 페이지에서 매번 새로운 데이터를 GET 요청으로 받아오는 것은 불필요한 리소스 낭비로 이어지기 때문에, 이를 Redux-toolkit 을 이용한 메모리 캐싱을 통해 관리하였습니다.
 
그러나 해당 로직의 문제점은 사용자가 새로고침을 하는 순간 상태가 빈 배열로 초기화된다는 점 입니다.

 

리덕스 툴킷을 이용한 전역 상태를 기반으로 렌더링된 세부 페이지(새로고침 전)

참고로 24.05.28 기준, 리덕스 툴킷은 제거되었습니다. 기존에 Recoil 을 사용하고 있고, 추가적으로 Tanstack-query/react 를 사용하고 있는 상황에서 리덕스 툴킷이 현재 프로젝트에서는 불필요하다고 판단했기 때문입니다.

(24.05.28 기준) 해당 디자인은 로직 개선과 함께 변경될 예정 입니다.

새로고침 후 보여지는 세부 페이지

GIF

 
 
이렇듯, 빈페이지가 보여짐에 따라 사용성을 저해하는 문제가 발생했고, 새로고침 이후에도 캐시 처리된 데이터가 유지될 수 있도록 해야하는 상황 이었습니다.
 

개선과정-

도구 선택

앞서 살펴본 문제상황에 따르면, 새로고침 이후에 임시적으로 할당되어 있던 메모리가 초기화된 부분이 문제이므로, 이를 유지할 수 있는 몇 가지 도구를 찾아보았습니다.
 
일단 브라우저 캐싱처리 용도로 사용할 수 있는 세션과 로컬 스토로지가 있고, 브라우저 내에서 데이터베이스 역할을 수행할 수 있는 indexedDB 라는 것이 있습니다. 또한 URL 을 기반으로 Key-value 형식으로 저장하는 방식, 쿠키를 이용한 방식 등도 보였습니다.
 
저는 이 중에서 브라우저 스토로지인 세션과 로컬 스토로지를 선택했습니다.

다른 방식을 배제한 이유
- indexedDB는 초기 설정이 생각보다 까다롭고, 대규모 데이터를 다루는 용도로 적합하지만, 너무 대규모의 데이터를 저장한다고 가정하더라도 초기렌더링 성능에 부정적 영향을 줄 수 있으므로 배제하였습니다. 

- 쿠키를 사용하는 방식은 서버로 데이터를 요청하는 경우 해당 쿠키의 데이터도 같이 전달되는 네트워크 리소스를 낭비할 수 있는 문제가 존재하여 배제하였습니다.

- URL을 사용한 방식은 키-값 쌍이 비교적 짧은 경우는 괜찮겠으나, 긴 배열을 담아서 저장하는 경우 사용자가 URL을 통해 페이지 전환 등의 작업을 시도한다고 가정하면 사용성을 저해하는 문제가 있기 때문에 배제하였습니다.

 

비록 5MB 의 크기 제한이 있으나, 레시피 데이터 자체가 5MB 를 넘어설 정도로 큰 데이터는 아니고, 다른 캐싱처리 방식에 비해 사용성도 좋다고 판단했기 때문입니다.


TMI
해당 스토로지의 Get 과 Set 를 편리하게 여러 곳에서 재사용할 수 있도록 해당 함수를 모듈화 하기로 하였습니다.

재사용성을 높이기 위한 함수 모듈화 | setStorage 함수 선언

타입스크립트를 사용하므로 해당 스토로지의 유형을 명확하게 구분할 수 있도록 enum (열거형)을 사용하여 LOCAL 과 SESSION 을 구분토록 했고, key 와 value , type 을 { } 로 묶어서 매개변수로 받아오도록 처리했습니다.

export enum StorageType {
    LOCAL = 'LOCAL',
    SESSION = 'SESSION'
}
interface SetStorageType {
    type: StorageType
    key: string
    value: any
}

export function setStoreage({ type, key, value }: SetStorageType) {

    if (type === StorageType.LOCAL) {
        window.localStorage.setItem(key, JSON.stringify(value))
    }

    if (type === StorageType.SESSION) {
        window.sessionStorage.setItem(key, JSON.stringify(value))
    }
}

 

재사용성을 높이기 위한 함수 모듈화 | getStorage 함수 선언

Setter 역할을 하는 함수를 통해 저장된 데이터를 불러올 수 있도록 getStorage 함수도 정의했습니다. 해당 함수의 경우도 인자로 type 과 key 를 전달받는데 앞서 setStorage 와 다른 점이 있다면 각 타입의 스토로지에서 데이터를 읽어오는 로직을 getLocalStorageItem 과 getSessionStorageItem 으로 모듈화하였다는 점입니다.

function getLocalStorageItem(key: string) {
    return window.localStorage.getItem(key)
}

function getSessionStorageItem(key: string) {
    return window.sessionStorage.getItem(key)

}

/**
 * 스토로지에 저장된 데이터를 읽어온다.
 * @param type 스토로지 타입
 * @param key 스토로지에 저장된 데이터의 키
 * @returns 데이터 반환
 */
export function getStoreage(type: StorageType, key: string) {
    try {
        if (type === StorageType.LOCAL) {
            const value = getLocalStorageItem(key)
            if (value) return JSON.parse(value)

        }
        if (type === StorageType.SESSION) {
            const value = getSessionStorageItem(key)
            if (value) return JSON.parse(value)
        }

        throw new Error(`현재 저장소에 존재하지 않는 키를 전달하였습니다. 전달된 키는 "${key}" 입니다.`)
    } catch (error) {
        console.error(error)
        return '잘못된 키'
    }
}

 
 

아래 로직만 다시 살펴보면,  try ~ catch 로 감싼 후 해당하는 key의 value 를 읽어오지 못했다면 에러를 띄우도록 예외처리를 해주었습니다.

export function getStoreage(type: StorageType, key: string) {
    try {
        if (type === StorageType.LOCAL) {
            const value = getLocalStorageItem(key)
            if (value) return JSON.parse(value)

        }
        if (type === StorageType.SESSION) {
            const value = getSessionStorageItem(key)
            if (value) return JSON.parse(value)
        }

        throw new Error(`현재 저장소에 존재하지 않는 키를 전달하였습니다. 전달된 키는 "${key}" 입니다.`)
    } catch (error) {
        console.error(error)
        return '잘못된 키'
    }
}

 

적용하기 |  레시피 데이터를 조회할 때 setStorage 를 호출

이제 새롭게 도입된 캐싱로직을 적용할 차례입니다. 우선 레시피 데이터를 조회하는 순간 해당 데이터를 sessionStorage 에 저장하도록 로직을 추가했습니다. 아래 함수의 경우 Recipe 페이지의 루트에 해당하는 컴포넌트 입니다. onSearch 라는 함수가 호출되면 외부 백엔드로 GET 요청을 하게 되고, 그 역할을 하는 것이 getFetchRecipeData 라는 함수 입니다. 그리고 이 함수가 반환하는 Recipes 와 totalCount 를 setStorage 의 인자로 전달하여 저장합니다.

// src/pages/Recipe/Recipe.tsx 

 /** 버튼 검색 액션 */
  async function onSearch(e: MouseEvent<HTMLButtonElement>) {
    const input = e.currentTarget.previousElementSibling as HTMLInputElement
    const productName = getSearchValue(input) || ''
    const { totalCount, recipes } = await getFetchRecipeData(productName)

    setProductName(productName)
    setRecipeInfo({ totalCount, recipes })
    setStoreage({ type: StorageType.SESSION, key: 'recipes', value: { recipes, totalCount } })
  }

 

저장이 되었는지 확인해보니 정상적으로 저장이 되었습니다.

 

적용하기 | 레시피 세부 페이지에서 useEffect 를 이용한 getStorage 호출

그 후 캐싱된 데이터를 재사용하기 위해 RecipeDetail 페이지의 루트 컴포넌트로 와서 사용합니다. 현재 키가 'recipes' 로 되어 있기 때문에 이를 getStorage(_, 'recipes') 형태로 전달해주었고, 그 반환값을 recipeInfo 변수에 담고 있습니다.

// src/pages/Recipe/RecipeDetail.tsx
import { useParams } from 'react-router-dom';
import { StorageType, getStoreage } from '@/utils/storage';
 
 // --- 중간에 불필요한 로직 부분 생략 ---

const params = useParams();

/** 사용자가 선택한 레시피 반환 */
  function extractRecipe(recipes: RecipeType[], recipe_id: string) {
    return recipes.filter(recipe => recipe.RCP_SEQ === recipe_id)[0]
  }

  /** 레시피 업데이트 */
  function updateRecipeData(recipe_id: string = '0') {
    const recipeInfo: RecipeInfoType = getStoreage(StorageType.SESSION, 'recipes')
    const recipe = extractRecipe(recipeInfo.recipes, recipe_id)
    setRecipe(recipe)

  }
  useEffect(() => {
    updateRecipeData(params.id)
  }, [params.id])

 
다만 현재 로직에서 recipeInfo 의 경우 totalCount 라는 레시피 전체 개수를 나타내는 속성을 가지고 있고, 이는 현재 페이지에서는 불필요 합니다. 또한 레시피 목록 전체가  필요한 것이 아니고 사용자가 선택한 레시피에 대한 데이터만 필요하므로 이를 처리하기 위해 extractRecipe 함수를 만들어 주었습니다.

/** 사용자가 선택한 레시피 반환 */
  function extractRecipe(recipeInfo: RecipeType[], recipe_id: string) {
    return recipes.filter(recipe => recipe.RCP_SEQ === recipe_id)[0]
  }
부연 설명
여기서 recipe_id 의 경우에는  react-router-dom 의 useParams() 를 호출하여 얻은 {id:"135"} 에서 value에 해당하는 예시의 135 와 같은 값만을 추출하여 전달받고 있고, recipe 목록 중에서 RCP_SEQ 와 일치하는 경우만 배열로 반환해주고 있습니다.

 
 

그리고 이렇게 만들어진 함수를 useEffect() 내에서 호출처리하는데, 여기서 중요한 부분은 params.id 를 의존성 배열에 넣어줌으로써 사용자가 향후 이전 레시피 혹은 다음 레시피를 클릭하여 /:id 부분이 변경되었을 때 새로운 데이터로 상태를 갱신해주도록 하는 것입니다.

  useEffect(() => {
    updateRecipeData(params.id)
  }, [params.id])

  

성과 및 앞으로 고려할 점

해당 로직을 적용한 이후에는 더 이상 빈 화면이 렌더링되는 일이 발생하지 않게 되었습니다. 다만, 현재의 경우에는 레시피 데이터의 크기가 크지 않기 때문에 문제는 없으나 5MB 를 넘어서는 경우에 발생할 수 있는 문제에 대한 예외처리도 필수적으로 고려되어야 하는 부분인 것 같습니다. 이 부분은 차후 개발이 완료된 이후에 어떻게 처리할 지 고민해볼 예정입니다.

 

 

참고로 현재 레시피 세부 페이지 디자인은 2024.05.28 기준으로 변경되었습니다.

영상에 잡음이 조금 들어갔네요. 0:05 초 부근에 음소거 처리가 안 된 것 같습니다.

 

 

[빌드 문제] CRA 의 느린 빌드 속도 및 유지보수 미흡으로 인한 높은 취약성 문제

문제상황

해당 프로젝트는 CRA(Create React App) 으로 처음 빌드되어 구축되었습니다.
 
처음에는 개발 서버를 구동하는데 걸리는 빌드 시간에 의미를 부여하지 않았으나, Vite 기반의 간단한 애플리케이션을 만들어보면서, CRA의 느린 빌드 속도에 큰 불만을 가지게 되었습니다. 특히, CRA 의 경우 변동 사항을 반영하여 렌더링되는데 체감상 Vite 기반의 앱에 비해서 1초 이상 차이가 났습니다.
 
왜 이런 차이가 나는지 공식문서를 살펴보니, 일반적인 웹펙과 같이 의존성 모듈 전체를 재렌더링하는 것이 아니라, 변동사항이 존재하는 모듈만 빠르게 대체하기 때문임을 알게 되었습니다. 여러모로 바꾸지 않을 이유가 없었습니다.
 
무엇보다 CRA는 react-script 패키지와 관련한 취약성 문제가 계속 발생하고 있음에도 불구하고, 개선하고자 하는 움직임이 보이지 않았습니다. 이에 대한 심각성을 바탕으로 빠른 시일 이내에 vite 기반으로 마이그레이션 하자는 목적을 가지게 되었습니다.
 

개선과정

개선방법 선택 : CRA 에서 VITE 로 빌드 환경 마이그레이션

CRA 와 VITE 기반의 앱의 초기 프로젝트 환경의 구성을 비교하면서, 그 차이점을 기반으로 마이그레이션을 진행하였습니다.
 
간략히 거친 과정을 언급하자면 다음과 같습니다.

package.json 에서 CRA 기반의 비의존성 및 의존성 패키지 정리 → vite 설치 및 플로그인 설치 → vite.config.ts 설정 → react-app-env.d.ts 설정 → 환경변수 → eslint 설정 → 빌드 과정

 

과정1 | vite.config.ts 파일 추가

vite 의 경우에는 vite.config.ts 파일을 추가하고, 해당 vite 앱이 react 기반임을 vite에 알리고, 빌드 시 해당 파일이 어디에 저장될 것인지를 기본값으로 설정해야 했기에 다음과 같이 플로그인 등록과 경로 설정을 실시하였습니다.
 

 

과정2 | 환경변수의 타입을 타입스크립트가 추론할 수 있도록 지원하는 react-app-env.d.ts 파일 수정

환경변수의 타입을 지정하는 react-app-env.d.ts 파일의 경우에는 기존 CRA 설정 방식이었던 reference를 vite 기반으로 변경하였습니다.

CRA 에서
VITE 에서

 

과정3 | process.env 를 Vite 기반의 import.meta.env 로 수정

환경변수 파일의 경우 vite 기반의 리액트 앱에서는 환경변수에 접근할 때 imprt.meta.env 로 접근하여 사용함을 확인하였고, 다음과 process.env 부분을 vite 환경변수에 맞게 수정하였습니다.

참고로 process.env 혹은 import.meta.env 로 접근할 시 타입스크립트가 해당 환경변수에 대한 타입을 추론할 수 있도록 하는 파일이 앞서 설정한 react-app-env.ts 입니다.

 

 
 

과정4 | 불필요한 %PUBLIC_URL%/  을 삭제하기 위해 index.html 파일 수정

또한, index.html 파일의 경우에도 변동이 발생하였는데, CRA에서는 public 폴더 접근 시 %PUBLIC_URL%/ 로 접근하였으나, Vite 에서는 이 부분이 생략되어도 내부적으로 public 폴더로 설정되는 것을 확인하였고  변동사항을 체크 후 이제는 사용하지 않는 %PUBLIC_URL%/  을 제거하 작업을 수행하였습니다.
 

 

결과 | 변경 사항을 반영한 의존성을 재설정하기 위한 처리 및 빌드 실행

이후 이전 기록이 남아있는 node_modules 폴더를 rm -rf 명령어를 통해 하위 디렉토리 까지 모두 강제 삭제 처리하고, npm install 을 통하여 변경된 종속성을 재설치 하였습니다.
 
그 후 빌드 명령어를 실행하여 성공적으로 마이그레이션이 되었음을 확인하였습니다. 

 

성과

- 체감상 빌드 속도와 코드 변경 사항에 대한 리렌더링에 있어서 CRA 보다 Vite 가 효율적이고 빠르게 느껴졌습니다. CRA 에서는 파일 자체가 그리 큰 사이즈가 아님에도 변경 사항이 반영되는데 약간의 지연을 느꼈다면, Vite 에서는 지연됨이 거의 느껴지지 않을 정도 였습니다.
- 개발 서버의 빌드 속도 또한 눈에 띄게 증가하였습니다. 배포 파일 빌드 시 빌드 시간이 감소 하였는데, webpack: 18.35s → vite: 5.66s 으로 크게 감소하였습니다. 다만 수치적으로 측정하는 부분은 매번 오차가 있어서 명확하지는 않습니다.
- 끝으로, 이번 마이그레이션의 주요 이슈였던 CRA 에서 뜨던 react-scripts 취약성 문제가 해결되었습니다. 

 


[성능최적화]  루트 페이지 접속 시 랜더링이 지연되는 문제

문제상황.

루트 페이지를 접속하면, 완성된 화면을 렌더링 하는데 오랜 시간이 걸리는 문제가 발생하였습니다. 개발 서버인 것을 감안하더라도 50점 밑의 점수는 성능 최적화가 반드시 필요한 부분이라 판단하였습니다. 
 

 

그리고, 크롬 브라우저의 라이트 하우스로 성능 측정을 해보니 , 빌드 이후의 bundle.js 파일이 3.082.0 KiB로 매우 컸기에, 렌딩 페이지와는 연관이 없는 js 파일까지 다운로드 하는데 많은 시간이 소요된 것으로 보였습니다. 사실 체감상으로 본다면, 그리 느리게 느껴지지는 않았으나, 약간 지연된다는 느낌을 무시할 수는 없는 것 같습니다.
 

 

 
FCP 가 4.8초로 매우 느렸고, 그에 따라 LCP 의 경우도 5.5초, TBT의 경우 560 ms 로 그 동안 사용자가 사이트를 방문할 시 오랫동안 동작되지 않는 화면을 보게 되는 문제에 직면했습니다. 물론 TBT의 경우 1초 미만이라면 느린 편이라고 볼 수는 없으나, 사이트의 확장성 및 간단한 사이트임을 감안하면 이 정도 시간이 걸리는 것은 매우 좋지 못한 경우이라 보았습니다.

제가 이해한 각 용어의 정의를 풀어서 정리해보면 다음과 같습니다.
FCP : 처음으로 의미 있는 요소가 화면에 그려지는데 걸리는 시간
LCP : 요소 중 가장 커다란 요소가 처음 화면에 그려지는데 걸리는 시간
TBT : 사용자가 UI 와 상호작용하는데 제한이 걸리는 총 시간

 

프리뷰(프로덕션) 환경에서도 측정했었고 약 70점 정도 나왔던 것으로 기억합니다. 스크린샷으로 찍어두었는데 어디 갔는지 보이지 않아서 아쉽네요.

 

개선과정.

원인분석

결국 빌드 이후의 번들 파일이 컸기 때문에, 브라우저 입장에서는 사이트에 접속 시 그 커다란 파일을 모두 다운로드 함에 따라 지연시간이 발생한 것으로 보았습니다. 따라서 필요한 페이지 내에서만 해당 모듈을 import 하여 다운로드하도록 하는 코드 분할 기법을 적용하기로 하였습니다.
 
코드 분할이 무엇인지 이론상으로만 알았는데, 실제 적용하게 되니 어떤 변화가 생길지 많이 기대가 됩니다.
 

Lazy 와 Suspense | 리액트에서 제공하는 코드분할 기법을 위한 도구

리액트에서는 마침 코드 분할을 위해서 Lazy 함수를 이용한 동적 import 기능을 지원하고 있었고, 이와 함께 Suspense 컴포넌트를 활용하여 fallback 처리도 함께 할 수 있다는 이점을 바탕으로 이를 적용하기로 결정하였습니다.
 

코드 분할 기법 적용

react-router-dom 을 사용하고 있었기에, 라우트 별로 사용자가 해당 페이지에 접속할 때 파일을 다운로드할 수 있도록 라우트별로 lazy 함수로 동적 import 를 적용하고, 코드 분할을 실시하였습니다.

import { lazy,Suspense } from "react";

const Home = lazy(()=> import('../pages/Home/Home'))
const LocalFoodPage = lazy(()=> import('../pages/LocalFood/LocalFoodPage'))
const NutritionPage = lazy(()=> import('../pages/Nutrition/NutritionPage'))
const NecessitiesPage = lazy(()=> import('../pages/Necessities/NecessitiesPage'))
const HaccpPage = lazy(()=> import('../pages/Haccp/HaccpPage'))
const RecipePage = lazy(()=> import('../pages/Recipe/RecipePage'))
const RecipeDetail = lazy(()=> import('../pages/Recipe/RecipeDetail'))
const BmiPage = lazy(()=> import('../pages/Bmi/BmiPage'))

const router = createBrowserRouter([
  {
    path: "/",
    element: 
     <Suspense fallback={<PageLoading/>}>
      <Home/>
    </Suspense>,
  },
  {
    path: "/",
    element: <Header isStyle={true} />,
    children: [
      {
        path: "/localfood",
        element: 
         <Suspense fallback={<PageLoading/>}>
          <LocalFoodPage />
         </Suspense>
        ,
      },
       // -- 중략 --
]);

export default router;

 
막상 적용하고 보니, 남길게 별로 없는 것 같네요. 어디에 적용하면 좋을지 고민을 많이 했었는데, 딱 router 를 담당하는 파일 내부가 적합해 보였고, 이에 적용 했습니다.
 

성과.

우선, 코드 분할 기법을 적용 결과 다음과 같이 FCP 는 1.3 초, LCP 는 2.1 초로 성능이 크게 개선된 것을 확인할 수 있었습니다. 최종적으로 개발 환경에서 성능 측정 점수는 34점 에서 77 점으로 큰 증가폭을 보였습니다.
 
결과만 보면, 사이트 렌더링 속도가 빨라졌고, 데스크톱/모바일 환경 전체적으로 렌딩 화면 접속 시 장시간 멈추는 문제를 개선할 수 있었습니다.
 

 
 
배포 환경에서 재측정 해보니 FCP, LCP, TBT도 크게 개선되었고, 성능 점수가 91점으로 증가된 것을 볼 수 있었습니다. 코드 분할 이전의 배포 환경에서 성능 점수 77 점을 추정한 것과 비교하면 많은 개선을 경험하였다고 볼 수 있을 것 같습니다.
 

 

 
 
 

[렌더링 이슈] 렌더링 된 지도의 그래픽(이미지) 중첩 문제[일부 해결 사례]

문제상황..

지역별 향토음식과 시장을 소개하는 세부 페이지에서 해당 식당의 주소를 기반으로 지도에 마커를 표시하는 기능을 적용 했으나, 최초 렌더링 이후 지도에 하얀 화면이 중첩되어 표시되는 문제가 발생하였습니다 
 

 

해결과정..

원인분석

해당 문제가 발생한 원인을 찾아보기 위해 크롬 개발자 도구의 Element 탭을 확인 해보니 다음과 같이 동일한 요소가 중첩으로 렌더링된 것을 확인할 수 있었습니다.

 

 
 

실제 해당 문제가 화면에 렌더링되는 지도에 어떠한 영향을 미치는지 검증하기 위해 두 번째 <div/> 태그에 임의로 스타일 속성 display:none 을 추가해 보았더니, 정상적으로 지도가 렌더링되는 것을 볼 수 있었습니다.

 

그럼 중복으로 그려지는 해당 태그가 생기는 부수적인 원인을 찾기만 하면 되겠네요. 빨리 원인을 찾아서 다행입니다.


음.. 그런데, 여기서 갑자기 드는 의문점

위 문제를 알고 나서 한 가지 들었던 의문점은 문제의 페이지에서 리렌더링을 하면, 정상적으로 지도가 표시되기도 했다는 점 입니다. 이에 리렌더링 마다 페이지에 지도를 그리는 div 요소가 어떻게 동작하는지 개발자 도구의 Element 탭에서 다시 확인해보기로 하였습니다. 

 

계속 해서 중첩되고 있는 div 요소들

확인 결과 div 요소가 계속해서 중첩생성 되는 문제를 발견할 수 있었습니다

이 문제의 경우 코드에디터에서 파일 수정 시 생기는 중첩 문제이고, 다른 페이지에서 해당 페이지로 재접근 하는 경우에도 동일한 문제가 발생하고 있습니다. 

 

 
 현재 까지 발견한 문제점은 두 가지 였습니다.

첫 번째, 최초 렌더링 시 정상적인 지도 위에 백지의 지도가 중첩으로 그려지는 문제
두 번째, 리렌더링 시(페이지 재방문시 등) 마다 지도 그래픽을 그리는 div 요소가 게속해서 생성되는 문제

 
이 중에서 제일 만만해 보이는 첫 번째 문제 부터 해결해 보겠습니다.
 

[문제 개선] 우선,  최초 렌더링 시 지도가 중첩되는 문제 부터

첫 번째 문제의 경우에는 여러가지 시도를 해본 결과 매개변수로 전달받은 값이 정상적인 값인지 아닌지를 분별하지 않고 값을 사용함에 따라 발생한 것이었습니다.
 
따라서 이른 반환을 통해서 조건을 통과하지 못하는 경우에는 애초에 useEffect 내에 로직이 실행되지 못하도록 차단하기로 하였습니다.
 
보시면,  위도(la), 경도(lo),  주소(address) 의 경우 임의로 정한 최소 기준치를 넘어서지 못하면 지도를 렌더링하는 로직이 실행되지 못하도록 막을 수 있도록 하고 있습니다.

 
 

해당 로직을 적용해보니 이제는 백지 화면이 생기지 않고,  정상적으로 렌더링되고 있는 것을 확인할 수 있었습니다.

 

[문제 개선]  두 번째, 리렌더링 시 지도를 렌더링하는 그래픽 요소가 중첩되는 문제

이제 두 번째 문제입니다. 해당 문제는 코드 에디터에서 변경 사항을 저장할 때, 혹은 다른 페이지로 이동 후 다시 해당 페이지에 방문하는 경우, 기존에 렌더링 된 그래픽 요소 위로 새로 생성된 요소가 중첩해서 쌓이는 것이 었습니다.
 
여러 시도를 통해 알게된 해당 문제의 원인 중 하나는 kakao map sdk 를 추가하는 script 태그를  useEffect 내에서 동적으로 생성하고 있던 부분이었습니다(파고 팔수록 계속해서 보이는 문제들)
 

스크립트가 계속 생겨나고 있는 중

 
따라서 useEffect 의 클린업 함수를 활용해서  컴포넌트가 디마운트 될 때 script 태그를 제거해주도록 로직을 추가하였고,
 
  load 이벤트의 경우에도 계속 등록만 되고 있고, 해제되지 않고 있었기에 메모리 누수를 방지하기 위한 목적으로 이를 제거하는 로직도 같이 추가하였습니다.

    script.addEventListener('load', mapLoader)

    return () => {
      script.removeEventListener('load',mapLoader)
      script.remove()
    }

 
해당 로직을 추가하고, 리렌더링 시 스크립트 태그가 중첩해서 추가되지 않는 것을 볼 수 있었습니다.

 
 
스크립트 관련 이슈를 해결하고 나서, 다른 페이지에서 다시 현재 페이지로 돌아오는 경우 발생하는 div 요소 중첩 문제는 해결되었습니다.
 
그러나, 코드 에디터에서 파일저장을 하는 경우 발생하는 div 요소 문제는 해결하지 못했습니다. 즉, 이번 트러블 슈팅의 개선책은 반쪽 짜리라고 볼 수 있습니다.
 

성과와 변경점(25/5/27 변경사항 반영)

성과

- 결과적으로 지도 위에 지도가 렌더링 되는 문제를 개선할 수 있었습니다.
- 스크립트 태그를 동적으로 등록하는 이벤트 리스너가 제거되지 않아서 발생할 수 있는 메모리 누수 문제도 덤으로 해결하였습니다.
 

2024년 5월 27일 기준으로 변경된 점

앞서 트러블 슈팅에서 개선한 방식은 useEffect 가 호출될 때 마다 생성되는 스크립트를 디마운트 시 제거해주고, 이벤트 등록을 취소하는 방식으로 메모리 누수와 지도 svg 중첩 문제를 어느 정도 해결했었습니다. 그러나 24년 5월 25일 기점으로 리팩터링을 진행했고, Vite 에서는 index.html 파일에서 환경변수를 %env% 형식으로 접근할 수 있으므로 굳이 해당 컴포넌트 내에서 스크립트를 생성할 필요가 없었습니다.

 

따라서 index.html 파일 내에서 아래와 같이 스크립트 태그를 head 태그의 마지막 자식 요소로 추가해주었습니다.

    <script
      type="text/javascript"
      src="//dapi.kakao.com/v2/maps/sdk.js?appkey=%VITE_KAKAO%&libraries=services,clusterer"
    ></script>

 

 

 

반응형