본문 바로가기

프로젝트/복지맵(중단)

[복지맵 프로젝트] 트러블 슈팅 모음집 ①

반응형

오늘의 명언 "모든 문제에는 실마리가 존재한다."


 

[리셋 문제 1 ] 필터 모달을 닫는 경우 체크 항목이 초기화되는 문제 

해당 문제는 단순하게 해결되며, [리셋 문제2] 로 이어집니다.

문제상황,

사용자가 필터 기능을 사용하여 검색을 하고, 해당 창을 닫은 후 다시 사용하려고 하면, 기존에 체크 영역이 초기화되는 무제가 있습니다.

필터창을 열고 체크한 상태

 

[닫기] 후 다시 필터창을 연 후

 

개선과정,

원인분석

해당 문제의 1차적인 원인은  showFilters 라는 true or false를 담고 있는 상태가 변경될 때 마다 해당 필터창이 브라우저 렌더링에서 완전히 배제되었다가 생겨나면서 생기는 문제 입니다. 

 

이 문제를 해결하는 단순한 방법은 showFilters 상태에 따라서 해당 필터 모달의 클래스를 동적으로 추가 혹은 제거하는 방식으로 해결할 수 있을 것이라 판단 하였습니다.

 

클래스의 동적인 추가/삭제를 기준으로 필터창의 가시성 확보

기존의 && 연산자를 통해 필터 전체를 렌더트리에서 지운 것과는 달리 가시적으로 보이지 않도록 설정하여 기존에 유저가 선택한 항목이 초기화 되지 않도록 변경하였습니다.

결론적으로,

결론적으로 해당 문제는 간단하게 해결 되었습니다. 그러나 여기서 추가적인 문제가 발생하였습니다. 사용자가 새로고침을 하는 경우에는 필터 항목이 모두 초기화되는 것은 막지 못했던 것 입니다. 이 문제를 다음 챕터에서 해결해보았습니다.

 

 

[리셋문제 2 ] 새로고침을 하면 사용자가 선택한 항목이 초기화되는 문제

문제상황

앞서 [리셋문제1] 에서 && 연산자를 사용한 초기화 문제를 개선하기 위해 className 의 동적인 추가 및 제거를 통해 요소의 가시성을 확보하는 방식으로 문제를 개선했었습니다.

 

그러나 새로고침을 하는 경우에는 다시 선택한 필터항목이 초기화되어 사용자가 다시 해당 항목을 필터하여 재검색해야 하는 문제가 남아 있었습니다.

 

개선과정

원인분석

해당 문제는 결국 캐싱이 제대로 되어 있지 않아서 발생한 문제 입니다. 이 문제를 해결하기 위한 방안으로 몇 가지 생각해보면, 다음과 같습니다.

- localStorage 혹은 sessionStorage 를 활용한 캐싱
- cookie 를 활용한 캐싱
- 쿼리 파라미터를 활용한 캐싱

 

도구선택

앞서 생각한 방식 중에서 처음 사용해보는 쿼리  파라미터를 사용하여 캐싱해보는 방식을 적용해보기로 하였습니다. react-router-dom 에서는 쿼리 파라미터에 쉽게 접근 가능한 useSearchParams 라는 훅을 제공해주고 있어서, 이를 활용해보기로 하였습니다.

 

해당 훅에 대해 간략하게 설명하면, useState 와 동일한 형식으로 첫 번째 배열의 요소는 params 를 담고 있는state 를 가지고 있고, setSearchParams는 해당 state인  searchParams 를 업데이트 할 수 있는 setState 메서드를 반환합니다. 

 let [searchParams, setSearchParams] = useSearchParams();

 

이를 활용하면 사용자가 선택한 필터 항목을 쿼리 파라미터로 저장하고, 새로 고침 후에도 쿼리 파라미터에는 선택한 항목이 주소로 남아 있기 때문에 현재 경험하고 있는 문제를 쉽게 해결할 수 있습니다.

 

쿼리 파라미터 설정 함수 정의

앞서 searchParams 의 두 번째 반환값인 setSearchParams 를 사용하면 실제 쿼리 스트링을 설정할 수 있습니다.  아래 함수의 경우에는 [검색] 이라는 버튼을 클릭하면, 필터된 항목을 쿼리 스트링으로 저장하는 함수 입니다.

  /** onClick: 쿼리 스트링 설정 */
  const onClickSetSearchParams = () => {
    setSearchParams('targets=' + targets + '&regions=' + regions);
  };

 

전체 로직은 보여드리기 힘들지만, 해당 로직이 어떻게 동작하는지 영상 자료를 준비해보았습니다. 사용자가 각 필터 항목을 선택하고, [검색] 을 클릭하면 상단의 주소창에서 쿼리 파라미터가 추가되고 삭제되는 것을 볼 수 있습니다.

 

현재의 문제점

현재 쿼리 파라미터가 설정되고 해제되는 것은 정상적으로 동작하지만, 새로고침하면 체크된 항목이 초기화되는 것은 동일하게 발생합니다. 즉, 설정한 쿼리 파라미터를 읽어온 뒤 이를 필터 항목의 checked 속성과 동기화 시켜줄 필요가 있습니다.

 

쿼리 파라미터 읽어보기 및 전처리

현재 setSearchParams로 설정한 값을 읽어오기 위해서는 searchParams 객체가 보유하고 있는 get() 메소드를 사용해야 합니다. 또한, get 으로 읽어온 값(value)은 문자열 형태(ex. '전체,노년,중장년') 로 읽어오기 때문에, 각 항목을 배열 형태로 분리하여 사용하기 좋게 재가공해주어야 합니다.

const [searchParams, setSearchParams] = useSearchParams();
// ex. searchParams.get('targets')

 

따라서 해당 쿼리 파라미터를 쉼표(,) 를 기준으로 분리하기 위해 .split( ) 메소드를 사용하고, targets 와 regions 로 구분되어 있는 키들의 값을 하나의 배열로 합쳐 주기 위해 다음과 같이 로직을 작성해주었습니다. 이렇게 되면, targets 와 regions 의 값들을 하나로 합친 배열 형태로 값을 반환해주게 됩니다.

const targetParams = searchParams?.get('targets')?.split(',') || []
const regionParams = searchParams?.get('regions')?.split(',') || []

const params: string[] =[...targetParams, ...regionParams]

console.log(params) // 출력시 ['전체', '중장년'] 와 같은 형태로

 

체크박스와 동기화 작업

앞서 전처리한 params 를 사용해서 이제 체크박스의 checked 속성과 동기화 시켜주기면 하면 됩니다. checked 의 경우 true 로 지정 시 해당 항목에 체크 표시가 되는 단순한 속성입니다.

 

params 는 필터링 기능을 담당하는 컴포넌트인 GovermentServiceFilter 의 prop 으로 전달되고, 이를 filterList 라는 변수에 재할당하였습니다. 가독성을 높이기 위해 그런 것인데, 사실 이렇게 할 필요 없이 params 이름 자체를 바꾸거나 그대로 사용하는게 더 나아 보입니다.

 

 

해당 prop의 경우 여기서 건드려줘야 하는 부분은 input 의 defaultChecked 라는 속성 입니다. 기존의 checked 를 사용 하는 경우에는 하드코딩이 된 것처럼 사용자가 값의 변경이 불가능해지는 반면(호환성 문제 같네요),

 

해당 속성을 사용하면 동기화가 되는 동시에 사용자가 값을 수정할 수 있게 해줍니다. 즉, 리액트의 JSX 문법 상에서만 사용가능한 속성입니다.

   <section className={styles.filter_section}>
          <h3>대상</h3>
          <div className={styles.checkbox_area}>
            {targetList.map((target) => (
              <label key={target} className={styles.checkbox_label}>
                <input
                  type="checkbox"
                  value={target}
                  name={target}
                  defaultChecked={filterList.includes(target)}
                  onChange={(e) => {
                    onChange(e, 'targets');
                  }}
                />
                <span>{target}</span>
              </label>
            ))}
          </div>
   </section>

 

결과적으로

결과적으로 이제는 새로고침을 하더라도 사용자는 자신이 선택한 옵션이 초기화 되는 최악의 문제를 경험하지 않게 되었습니다. 다만 해당 방식의 경우 URL 이 너무 길어짐에 따른 복잡성이 조금 거슬리기도 합니다. 현재로서는 이 방식이 가지는 효율성과 사용성이 로컬이나 세션등의 스토로지를 이용한 방식 보다 간편하다고 보기에 이 방식을 그대로 채택하여 이어가 가볼까 합니다.

 

 

 

 

 

 

 

[빌드 실패] GSAP 모듈에 대한 타입스크립트 모듈 인식불가로 인한 빌드 실패문제

문제상황.

 gsap 모듈에서 타입스크립트 모듈을 인식하지 못하는 문제로 인해서 npm run build 이후 빌드가 실패하는 문제가 발생하였습니다. 

 

 

 

개선과정.

에러 로그 확인

현재 빌드 이후 에러 로그를 살펴보면 gsap/all 과 gsap/Flip 의 경우 타입스크립트 선언 파일을 찾을 수 없다고 알려주고 있 습니다. 이에 대한 해결 방안으로  npm i --save-dev @types/gsap 를 설치하면 해결이 된다고 하였으니, 이를 적용해보기로 하였습니다.

 

 

타입 선언 파일 설치 후 빌드, 결과는 실패

그러나 타입 선언을 설치한 이후에도 이 문제가 해결되지 않는 것을 확인할 수 있었습니다. 

 

 

 

설치하는 것을 추천하기에 설치를 해보았지만, 제가 알기로는 gsap 는 자체적은 타입 선언 모듈 파일을 제공하기 때문에 @types/gsap 설치가 필요 없는 것으로 알고 있는데 왜 이런 에러가 발생하는 걸까요?

 

 

타입스크립트 컴파일 옵션에 GSAP 타입 파일을 인지하도록

보통 타입스크립트 구성에 대한 설정은 tsconfig.json 에서 이루어지기 때문에, 해당 파일을 찾아보았습니다.

 

공식문서( https://www.typescriptlang.org/ko/tsconfig/#typeRoots ) 에 따르면, typeRoots 라는 속성으로 지정하면 명시적으로 해당 경로에 있는 폴더 혹은 파일들만 전역적으로 인식할 수 있다고 합니다. 현재 타입 선언이 알 수 없는 문제로 인해 전역적으로 접근하지 못하는 상황이라고 한다면, 이를 직접 명시하여 해결할 수 있을 것이라 생각했습니다.

 

그래서 typeRoots 속성을 추가하고, 전역적으로 타입 선언 파일을 인식할 경로를 각각 지정해주었습니다.

 

 

 

 

하지만, 이 또한 실패하였습니다. 해당 경로를 직접 명시해주어도, gsap 모듈은 타입 모듈을 인식하지 못하는 문제가 계속 발생하였습니다.

 

 

 

도대체 무엇이 문제일까요? 

현재 gsap 의 타입선언 모듈은 모두 node_modules/gsap/types 에 모여 있습니다.  타입스크립트는 해당 파일들을 전역적으로 접근하지 못해서 계속해서 에러를 보내주고 있던 거죠. 이에 앞서 tsconfig.json 에서 typeRoots 를 지정하여 명시적으로 해주었으나, 그럼에도 해결되지 않았습니다.

 

 

 

그럼 이를 직접 경로에 접근하여 명시하면 어떻게 될까요? 이제 직접 해당 경로에 접근하니 에러가 사라졌습니다.

 

 

뭔가 느낌이 좋습니다. 이 때 npm run build 를 하면 어떻게 될까요? 

 

 

에러 메시지가 잘 보이지는 않지만, 해당 타입 선언에 대한 모듈 파일로 인해 빌드할 수 없다는 에러가 발생하였습니다. 일단 에러 메시지가 달라진 것만으로도 유의미한 시도였다고 봅니다.

 

그럼에도 문제해결의 실마리를 찾았습니다.

일단 기존 gsap 에 대한 타입 에러가 사라졌다는 점은 결국 유의미했습니다. 결국 타입 선언 파일만 어떻게든 인식되도록 타입스크립트에게 알려주기만 하면 됩니다. 

 

우선 제가 앞서 했던 실수를 말씀드리면, gsap/types 에 대한 접근을 컴포넌트 파일 내부의 최상단에서 했다는 것입니다. 앞서 tsconfig.json 에서 TypeRoots 를 사용했었는데, 제가 한 가지 놓친 부분이 kakao sdk 파일의 경우에는 .d.ts 파일을 명시적으로 인식하도록 하기 위해 "types" 라는  속성에 아래와 같이 사용하고 있었습니다. 즉, gsap/types 의 경우에도 해당 속성의 요소로 지정해둔다면 타입스크립트는 해당 경로에 있는 타입 선언 파일 모두를 인식할 수 있게 된다는 말입니다.

 
 {
   "compilerOptions": {
    "types": ["kakao.maps.d.ts"],
 }
 

 

따라서 "gsap/types" 를 옆에 추가하였습니다.

 

 

 

재빌드 시도

아까와 다르게 드디어 빌드가 성공하였습니다. 결국 이번 트러블 슈팅의 원인은 컴파일 옵션에 대한 저의 이해 부족에서 기인했던 것 같네요. 그럼에도 빠른 시간 내에 해결할 수 있어서 다행 이었습니다.

 

 

 

결론적으로.

일단, 이번 계기를 통해서 tsconfig.json 파일에 대한 추가적인 학습이 필요하다는 사실을 많이 느꼈습니다. 간단히 필요할 때 만 알아두면 된다는 생각을 가지고 있었는데, 기본이 되는 부분을 놓치는 경우 발생할 수 있는 문제가 많을 수 있음을 깨닫는 계기가 되었던 것 같네요. 

 

[페이지 증발] 데이터 로드 시 지연으로 인해 빈 페이지가 렌더링되는 문제

문제상황..

서버로 부터 구군별로 새로운 데이터를 받아오는 경우 새로운 데이터로 화면이 렌더링되지 않고, 빈 페이지만 로드되는 문제가 발생하였습니다.

 

이전에 해당 기능을 구현하고 나서 여러 번 테스트 했을 때 아무런 문제가 없었으나, 2024년 5월 5일 경 오후 9시에 기능 동작을 확인하니 해당 문제가 발견되었습니다.

 

 

개선과정..

해당 문제가 발생했던 근본적인 문제는 react-query 를 사용하여 읽어온 데이터가 선택된 구군에 따라 새로운 데이터를 다시 가져오는 약간의 지연 시간 동안에 data 변수에 담기는 값이 undefined 가 되었고, 이를 적절하게 처리하지 못해 발생한 문제였습니다.

 

이 문제가 발생할 시점에 이 부분을 고려하여 데이터가 받아온 뒤에 화면에 목록을 렌더링하도록 로직을 짜두었다고 생각했으나, 연속적으로 구군을 클릭하거나, 느린 네트워크 속도를 설정해두고 데이터를 불러오는 경우에 제가 미처 처리하지 못한 지점에서 결국 문제가 발생한 것입니다.

 

다음과 같이 Loading 을 보여주는 컴포넌트가 렌더링되도록 설정하였습니다. 이에 필요한 모든 데이터가 undefined 가 아니며, 로딩 상태가 아닌 경우에만 정상적으로 목록이 렌더링 됩니다( 현재 이 트러블 슈팅을 작성하고 있는 시점은 2024년 5월 12일 기준으로 이전에 개선된 로직에서 추가로 변경이 있어서 변경 로직의 일부를 가져와서 첨부해 보았습니다. ).

   const { data: centerServiceInfo, isFetching: centerIsFetching } = useFetchQuery(
    centerServiceDataUrl,
    'center',
    gugun,
  );

  const { data: localGovermentServiceInfo, isFetching: govermentIsFetching } = useFetchQuery(localGovermentServiceDataUrl, 'goverment')

if ((centerIsFetching || govermentIsFetching) && (!centerServiceInfo && !localGovermentServiceInfo)) return <Loading />
  return (
    <article id="second_article" className={`${styles.service_detail_wrapper}`}>
      <Heading title="서비스 상세" type="h2" />
      <div className={`${styles.service_list_wrapper}`}>
        <CenterServiceList serviceInfo={centerServiceInfo} />
        <GovermentServiceList serviceInfo={localGovermentServiceInfo} />
      </div>
    </article>
  );

 

결론적으로..

해당 문제를 트러블 슈팅으로 작성한 이유는 분명 페칭 상태 등을 적절하게 처리하였다고 생각 했으나, 데이터가 렌더링 되지 않고, 빈 화면을 보이는 문제가 발생하여 이에 대한 원인은 규명하는데 시간을 소요하였기 때문이며, 이에 대한 실수를 다시는 하지 않도록 명심하고자 하는 의미에서 작성되었습니다.

 

데이터가 기대되는 타이밍에 정상적으로 렌더링 되도록 하기 위해서 개발자는 다양한 시나리오를 생각하며 처리해야 함을 다시금 명확히 하는 계기가 되었고, 이 부분에 대해 너무 소홀하게 처리했음을 성찰하는 시간이 되었습니다.

 

 

반응형