[비정기적 기록] 신규 서비스 추가

SANGHYUN KIM·2025년 5월 28일
0

비정기적 기록

목록 보기
7/7

프로젝트를 하면서 마주쳤던 문제 및 생각들을 한번 정리한다.

저장소(상태)에 대한 고려

Flow를 보면 페이지에 걸쳐서 상태를 공유하면서 CRUD가 가능해야 한다. 그래서 어떤 저장소가 좋은 지 한번 고려를 해봤다.

sessionStorage로 활용하기

나의 전역상태 생성 기준

App 전체에서 활용되는 client 전역 상태는 다음 세가지 조건을 모두 만족할 때만 생성한다. 아래 조건은 실무경험 및 공부를 통해 정립되었다.

  • <Context />로 풀어낼 수 없는 목적인 경우
    • <Context />는 의존성 주입을 위한 도구이며, 외부에서 생성된 값을 하위 트리에 전달하기 위해 사용된다.읽기 중심의 값에는 적합하지만, 공유할 데이터가 자주 변경되거나 여러 위치에서 동시에 수정된다면 <Context />의 사용 목적과 맞지 않는다
    • 또한, Context는 렌더링 최적화를 위한 구조가 아니므로, 자주 변경되는 상태에 사용할 경우 성능 및 유지보수 측면에서 불리하다.
  • 공유 데이터가 여러 모듈에 의존되고 생명주기가 독립적이지 않은 경우
    • 공유하는 데이터가 손쉽게 <Context />로 전달이 가능한 경우 필요하지 않다.
    • 그러나 1개의 도메인이라도 컴포넌트 트리를 따라 전달하기 어려운 복잡도를 가지고 있다면 전역상태를 쓰는 것이 더 좋다.
    • 이렇게 될 경우 앱 구조를 봤을 때 전역상태의 사용처를 바로 파악하기는 어려우나 문서나 주석을 통해서 사용처에 대항 정보를 기재한다면 충분히 해결가능하다고 본다
  • tanstack-query를 통한 서버상태로 해결이 불가능한 구조
    • tanstack-query의 서버 상태를 캐싱하고 동기화하기 위한 도구다.
    • 값을 client에 이중 저장하지 않고 서버 상태로 저장 및 관리함에 따라 클라이언트 고유 상태를 더 분별할 수 있다.
    • QueryCache에 있는 데이터가 활용 가능하고 믿을 만하다면 얼마든지 활용가능하다.

이번 신규 서비스 구조는?

그럼 이번 신규 서비스 업데이트에서 필요한 구조는 어떨까?

<ReduxStore>
	<Routes>
		<DeliveryAddress /> // 배송지 정보
		<ServiceA />
		<ServiceB />
		<ServiceC />
		<ServiceD /> // 상태 A
		<ServiceE /> // 상태 A
		<ServiceF /> // 상태 A
		<ServiceG /> // 상태 A, 배송지 정보
	</Routes>
</ReduxStore/>

내가 필요한 정보 교환 구간은 아래 두 개이다.

  • 상태 A를 신규 서비스인 <ServiceD />, <ServiceE />, <ServiceF />, <ServiceG /> 간 교환
  • 배송지 관련 정보를 <DeliveryAddress /><ServiceG />간 교환

먼저 배송지 관련 정보는 이미 전역상태가 있기에 이를 활용하기로 했다.

상태 A는 어떨까?

  • 초기에는 <Context />를 고려했으나, 상태 A는 사용자의 입력값처럼 자주 변경되는 값이기 때문에 적합하지 않다고 판단
  • 공유 범위가 동일 레벨의 4개 서비스 간에 국한되어 있어 복잡도 낮음
  • tanstack-query에 의존하는 상태가 아니라 유저의 input을 전부 가지고 있어야 하다 보니 client 상태로 판단

Client 상태이지만 전역 상태 조건에는 부합하지 않고, 서비스 신청 완료 시 필요 없는 정보이기에 메모리를 계속 잡아 두지 않는 것이 좋을 것 같았다. 따라서, sessionStorage에 저장하고 useSyncExternalStore를 활용하여 react 상태주기에 연결시키는 것이 좋아보였다.

session에는 JSON.stringify로 저장

useSyncExternalStore에 대한 자세한 설명은 공식 사이트에 들어가면 더 자세히 볼 수 있다.
내가 마주한 문제는 직렬화(JSON.stringify)로 저장하는 포멧으로 인해 발생했다.

API 구조에 맞게 session에 저장할 구조를 미리 만들고 이미지 파일들을 제외하고 나머지 값들이 저장이 잘 되는 것을 확인 후에 이미지 저장을 하려고 직렬화를 했는데, File이 빈 객체로 저장이 되었다.
직렬화가 가능한 값으로는 아래와 같이 인지를 하고 있었는데, FIle도 안 된다.

  • 원시값
  • 일반 객체, 배열, Date

결국, fileReader().onLoad를 통해서 직렬화된 image값을 저장을 하고, API에서는 File타입으로 전환을 해야 하기에 제출 전 File타입으로 복구를 다시 해줘야 하는 작업을 추가로 해줬다.

이미지 복원에 대한 로직

현재 프로젝트에서는 onloadend callback을 활용하여 처리했다.

const handleAddImage = (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files;
  if (!file) {
    return;
  }

  const fs = new FileReader();
  fs.onloadend = () => {
	   // 추가 로직
  };
  // https://developer.mozilla.org/en-US/docs/Web/API/FileReader#instance_methods
  fs.[instance_methods](file[0]);
};

나는 직렬화를 해서 session을 저장해야 하니까 아래와 같이 추가로직을 해서 session에 저장을 하고

const handleAddImage = (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files;
  if (!file) {
    return;
  }

  const fs = new FileReader();
  fs.onloadend = () => {
    const toSave: ImageType = Object.assign(
      { name: file[0].name },
      { uri: fs.result },
    );

    setImages(insertFirstAvailable(images, toSave));
  };
  fs.readAsDataURL(file[0]);
};

복원 작업은 아래와 같이 했다.

const { uri, name } = sessionImageInfo;
const byteString = atob(uri.split(",")[1]);
const mimeType = uri.split(",")[0].match(/:(.*?);/)?.[1];

// 1. 버퍼를 만들고
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);

// 2. 버퍼에 base64 디코딩한 값을 복사
for (let i = 0; i < byteString.length; i++) {
  ia[i] = byteString.charCodeAt(i);
}

// 3. 파일 복구
const restoredFile = new File([ab], name, { type: mimeType });

storage 용량 제한. IndexedDB로 전환

그러나 QA를 하던 중 아래와 같은 에러를 콘솔에서 발견했다.

Failed to execute 'setItem' on 'Storage': Setting the value of 'xxx' exceeded the quota

나는 저장소 용량을 간과하고 있었다. 해당 에러 문구는 session 저장소의 용량 초과를 뜻한다. session은 5 ~ 10mb 사이를 유지하는데, 이 용량이면 유저가 이론적으로 무한적으로 추가할 수 있는 구조에서 사진을 담기가 힘들거라고 들었다.

그래서 급하지만 한 번 도 사용해본 적이 없는 indexedDB를 도입하기로 했다. 그 전에 indexedDB의 용량을 확인한 결과 iOS 기준 1GB까지는 지원하지만 아래의 이유로 프론트에서도 이미지 용량을 조절할 필요가 있었다.

  • 유저는 물픔을 제한없이 등록할 수 있으며 각 물품에는 최소 1개, 최대 4개의 이미지 등록 가능
  • 고용량 사진 전달 시 API의 응답 속도 늦어짐 확인

indexedDB 연결

이 부분은 처음하기에 GPT를 이용했다. useSyncExternalStore는 동기적으로 값을 tracking하기에 비동기 코를 활용할 수 없다고 한다. 그래서 useEffectuseState를 활용을 하여 만들었다.

활용 도중에 한 쪽에서 업데이트 한 값이 다른 한쪽에서 업데이트가 되지 않는 다는 것을 알았고, 각 호출된 상태가 전역상태 처럼 한쪽의 업데이트가 다른 업데이트를 유발하지 않는 다는 것이었다. 따라서, 변경할 때마다 custom event를 만들고 이를 catch하여 업데이트 하는 방식으로 작성했다.

export const useIndexedDelivery = () => {
  const [dbData, setDbData] = useState<IndexedAppraisalDeliveryInfo | null>(
    null,
  );
  const [loading, setLoading] = useState(true);

  const DB_NAME = "appraisal-delivery-db";
  const STORE_NAME = "deliveryInfo";
  const KEY = "session-delivery";

  useEffect(() => {
    const init = async () => {
      const db = await new Promise<IDBDatabase>((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, 1);

        request.onupgradeneeded = () => {
          const db = request.result;
          if (!db.objectStoreNames.contains(STORE_NAME)) {
            db.createObjectStore(STORE_NAME);
          }
        };

        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });

      const tx = db.transaction(STORE_NAME, "readonly");
      const store = tx.objectStore(STORE_NAME);
      const getRequest = store.get(KEY);

      getRequest.onsuccess = () => {
        if (getRequest.result) {
          setDbData(getRequest.result);
          setLoading(false);
        } else {
          const initial = setInitialIndexedDelivery();
          const writeTx = db.transaction(STORE_NAME, "readwrite");
          const writeStore = writeTx.objectStore(STORE_NAME);
          writeStore.put(initial, KEY);

          setDbData(initial);
          setLoading(false);
        }
      };

      getRequest.onerror = () => {
        console.error("IndexedDB read failed:", getRequest.error);
        setLoading(false);
      };
    };

    init();
  }, []);

  /** saveToDB에서 날린 CustomEvent를 catch.
   * - saveToDB 내부에서 바로 setDbData(info)로 업데이트하면 전역상태처럼 구독한 컴포넌트가 업데이트 전부 업데이트 되지 않음
   * - 그래서 event로 날려서 구독한 컴포넌트에서 useEffect로 받아서 업데이트
   */
  useEffect(() => {
    const handler = (e: Event) => {
      const customEvent = e as CustomEvent<IndexedAppraisalDeliveryInfo>;
      setDbData(customEvent.detail); // 새로운 데이터로 업데이트
    };

    window.addEventListener("indexed-delivery-update", handler);
    return () => window.removeEventListener("indexed-delivery-update", handler);
  }, []);

  const saveToDB = async (info: IndexedAppraisalDeliveryInfo) => {
    const db = await new Promise<IDBDatabase>((resolve, reject) => {
      const request = indexedDB.open(DB_NAME, 1);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });

    const tx = db.transaction(STORE_NAME, "readwrite");
    const store = tx.objectStore(STORE_NAME);
    store.put(info, KEY);
    window.dispatchEvent(
      new CustomEvent("indexed-delivery-update", { detail: info }),
    );
  };

  return { dbData, loading, saveToDB };
};

이미지 리사이징 및 저장 타입

이미지 저장은 기존에는 base64를 사용했지만, Blob으로 전환한 이유는 다음과 같다.

  • base64 encoding을 사용했던 것은 session에 값을 저장하고 useSyncExternalStore에 동일한 값(출처)을 주려고 사용. 그러나 indexedDB에는 굳이 직렬화가 필요 없기에 타입은 자유
  • base64로 encoding시 용량이 증가. Blob은 그대로 저장 및 File로 변경 용이
    • base64 문자열로 저장을 위하여 직렬화, 서버 보내기 전 복구화 작업 추가 프로세스 요구

그러나, Blob이 무조건 좋지 않다. Blob을 <img src={imageSrc}/>에 넣으려면 URL.createObjectURL(blob)을 진행해줘야 하는데, 이 때 생성된 메모리를 명시적으로 URL.revokeObjectURL()하지 않으면 계속 남아있다. 따라서, 생성된 url을 관리를 할 수만 있으면 Blob을 활용하기 좋다.

나는 Image 컴포넌트를 따로 만들고 useEffect내부 return문에서 URL.revokeObjectURL()를 통해서 메모리 최적화를 진행해줬다.

이미지 리사이징은 webp로 진행을 했고 react-image-file-resizer를 활용하여서 최적화 시켰다.
그 결과 다음과 같이 고용량 사진이 98%(7.9MB ➡️ 102KB)이상 줄어든 것을 알 수 있다.

이미지 포함하여 API 호출

기존 API 구조는 application/json 포맷만을 허용하고, 이미지 데이터를 중첩 객체의 image 필드로 전달하는 방식이 불가능했습니다. 이에 따라, 먼저 이미지 제외 정보를 서버에 전달한 뒤, 각 항목에 대해 서버로부터 받은 id 값을 기반으로 별도로 이미지를 업로드하는 방식을 선택했다.

이렇게 분리한 이유는 서버가 멀티스레드로 동작하기 때문에, 클라이언트가 1번, 2번, 3번, 4번 순서로 요청을 보내더라도 서버에서는 먼저 처리 완료된 순서대로 작업이 진행되기 때문이다. 따라서 고유한 id를 활용해 이미지와 항목 간의 정확한 매핑이 가능하도록 했다.

또한, 다수의 이미지를 동시에 저장 요청했을 때 일부 이미지만 저장되는 문제가 발생했다. 이는 HTTP/2에서 동시 요청 수가 브라우저 기준 최대 6개로 제한되기 때문이었습니다. 이를 해결하기 위해 이미지 업로드 요청을 2개씩 짝지어 순차적으로 보내는 방식으로 조정했다.

const limit = 2;
const chunks = [];

for (let i = 0; i < forming.length; i += limit) {
  const chunk = forming.slice(i, i + limit);
  const chunkResults = await Promise.all(
    chunk.map((item) =>
      mutateAsyncRegisterImage({ id: item.simpleId, data: item.formData }),
    ),
  );
  chunks.push(...chunkResults);

이벤트 관련

MutationObserver 및 이벤트 등록 시점

문제 분석

QA 진행하다가 아래와 같은 항목이 달렸다.

안드로이드에서 뒤로가기 눌렀는데, 팝업이 안 뜹니다.

입사 후 대부분 웹 기반 어드민을 해왔고 첫 유저화면은 외부 협력사였다. 당시에는 협력사로부터 앱 기능 미지원이 기본 정책이었기에 history stack 관련 문제를 해결 못 했다.
위 경험으로 인하여 자사 앱에서 이런 문제를 해결하는 컴포넌트가 있는지 확인을 했고 아래와 같은 이벤트 등록 코드를 발견했다

해결 코드

useEffect(() => {
    const isCustomPath = Object.keys(customRoutes).some((pattern) =>
      matchPath(pattern, location.pathname),
    );

    const func1 = () => { ... }
    const func2 = () => { ... };

    // 전체 상태 감시를 위한 MutationObserver
    const observer = new MutationObserver(() => {
      if (func1()) {
        return;
      }
      if (func2()) {
        return;
      }

      sendGoBackMessage(isCustomPath ? "CUSTOM" : "DEFAULT");
    });
    observer.observe(document.body, {
      attributes: true,
      childList: true,
      subtree: true,
    });

    // 메시지 리스너 정의
    const listener = (event: MessageEvent) => {
	     .../
    };
    // 메시지 리스너 등록
    window.addEventListener("message", listener);

    // 컴포넌트 언마운트 시 observer와 메시지 리스너 해제
    return () => {
      observer.disconnect();
      window.removeEventListener("message", listener);
    };
  }, [location.pathname, navigate, dbData]);

요약하자면:

  • MutationObserver를 통하여 webview가 특정 route에 진입 여부 확인
  • 브릿지 함수를 활용하여 웹에서 앱에게 뒤로가기 방식 정의 전달
  • 앱에서 뒤로가기가 클릭되면 웹에 이벤트를 전달
  • 웹에서 등록된 이벤트 리스트 함수를 발동

이 코드를 통하여 특정 페이지에서 안드로이드 뒤로가기 버튼을 클릭했을 때 팝업과 관련 함수를 노출시킬 수 있었다.

관련 문제 추가 확인

추가 기능을 포함하여 등록된 페이지들은 검수 중에 특정 페이지에서 등록된 이벤트가 발생되지 않는 것을 확인했다. 문제를 확인한 결과 기존에 등록된 이벤트 리스너가 여전히 살아있어 안드로이드 뒤로가기 버튼이 다르게 작동하는 것을 확인했다.

MutationObserver는 분명히 원하는 tag하위에 있는 모든 변경점을 감지하는데, 페이지 이동 시 감지 못하는 것이 이상했다. 그래서 MutationObserver 내부에 감지되는 모든 것을 log해봤다.

const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    console.log("mutation observed:", mutation);
  }
  ...
}

logging되는 모든 것을 보면서 아래와 같이 생각했다.

  1. React내부의 변경점은 감지를 못 하는가?
    1. 아니다. document.body가 아닌 document.getElementById('root')를 observer에 등록해도 React가 변경하는 모든 tag들이 찍힌다.
  2. 근데 왜 특정 페이지들은 왜 감지를 못 하는가?
    1. 특정 페이지들의 공통점은 첫 진입 시 API 호출이 없는 단순 HTML 변경이다.
    2. 1번을 통해서 observer는 React의 활동을 감지하기에 특정 페이지 내부에서 useEffect를 활용하여 tag를 추가해보니 observer가 감지했다
    3. useEffect의 발동은 React의 commit단계 이후이다. 그러면 Observer 등록은 commit 이전에 등록을 해보면 되지 않을까?
  3. 그러면 이벤트를 HTML기준 DOM render 전 등록을 하면 되지 않을까?
    1. 그래서 useLayoutEffect를 사용해 이벤트 리스너를 선등록했고, 문제의 페이지들에서도 logging이 보이며 뒤로가기가 정상적으로 작동했다

이 QA를 통해서 배운 것은 이벤트의 등록 및 제거만 신경쓰는 것이 아닌 “언제 HTML에 추가”되는 지도 중요하다는 점이었다.

라이브러리 내부의 앱과 상반되는 action

문제 확인

배송지 추가 후 뒤로가기 시 페이지 이동이 되지 않습니다.

QA 내용을 봤을 때 history stack이 증가한 것으로 판단이 되었고 재현해보니 history stack이 증가한 것을 콘솔을 통해서 알 수 있었다.

해당 액션은 이벤트 핸들러 코드 쪽에서 발생하는 것 같았다.

const editMutaion = useEditMutaion()
const submitMutation = useSubmitMutation()

const handleSubmit = () => {
	if(isEditing){
		// 아래 mutation에서만 발생
		return editMutaion(payload)
	}
	return submitMutation(payload)
}

그래서 다음과 같이 진행했다.

디버깅 시작

  1. mutation 내부 뜯어보기
    1. mutation은 useCustomMutation으로 기본 useMutation을 한번 wrapping해서 사용하고 있다. 그러나 useCustomMutation 내부에서는 그 무엇도 history stack을 추가하는 것이 없었다.
    2. mutation내부에 있는 reduxdispacth가 문제일까? dispatch되는 모든 action을 tracking하고 redux-dev-tools을 확인하여 상태값 변경을 확인해도 history stack을 증가시키는 것이 없었다
  2. 전역 컴포넌트 및 상위 컴포넌트에서 반응하는 useEffect가 있는지 체크
    1. 페이지와 같이 렌더링 되는 컴포넌트 및 상위 컴포넌트도 확인해봤다. 또한, 각 상위 컴포넌트 내부에 useEffect를 체크해봤고 문제가 일어난 페이지 파일과 관련이 없는 것을 봤다.
    2. mutation의 key로 url path가 쓰이고 있기에 전체 검색을 해봤는데도 history stack을 증가시키는 것이 없었다
  3. react-router-dom을 통한 디버깅
    1. 2번에서 발견한 것이 없어서 앱 어딘 가에서 react-router-dom 동작일까 싶이서 아래와 같이 전역 컴포넌트를 만들어서 실행했다.

      // useNavigationTracker.ts
      import { useEffect } from "react";
      import { useLocation, useNavigationType } from "react-router-dom";
      
      export const useNavigationTracker = () => {
        const location = useLocation();
        const navType = useNavigationType(); // 'PUSH' | 'REPLACE' | 'POP'
      
        useEffect(() => {
          console.log("[NAVIGATION EVENT]", navType, location.pathname);
        }, [location, navType]);
      };
      

      그러나 mutaion을 발동하면 로깅이 찍히지 않지만 history stack이 늘어났다

  4. form 컴포넌트 내부 전부 console.log
    1. 3번까지 전부 발견하지 못 했을 때, form과 관련된 컴포넌트이기에 HTML element를 확인했다.
      그러나:
      1. 컴포넌트를 확인한 결과 실제 form관련 semantic tag는 아니고
      2. <div>로 이루어진 제어 컴포넌트였다.

    2. 이 때도 GPT랑 같이 디버깅을 하면서 어떤 HTML tag들의 기본 action이 history stack이 추가하는 지 같이 봤다.

      동작history stack 추가 여부설명
      <a href="/..."> 클릭✅ yes링크 클릭은 기본적으로 PUSH
      `<form action="/..." method="GETPOST">` + submit✅ yes
      window.location.href = '/...';✅ yes명시적 리디렉션
      history.pushState()✅ yes수동으로 stack 추가
      replaceState()❌ no현재 히스토리 항목 덮어씀
      window.location.replace()❌ no현재 히스토리를 교체
      navigate(..., { replace: true })❌ noReact Router에서 replace 옵션 사용
    3. 위 정보를 토대로 chrome developer tools의 elements 탭에서 tag를 조사했지만 찾을 수 없었다.

      1. 추후 찾아보니 <iframe />내부의 tag는 검색이 안 된단다고 한다
    4. 결국 제어 컴포넌트 내부에서 전부 console.log(history.length)를 추가하여 logging을 했고 주소록 검색에 사용되는 라이브러리 이벤트 발생 시 history가 쌓이는 것을 알 수 있었다.

  5. 라이브러리 뜯기
    1. 먼저, github issue에 동일한 증상이 있는지 확인을 했다. 그러나 관련 issue는 없었다.

    2. 크롬의 developer tools 내부의 elements tab에서 라이브러리가 그리는 HTML 전부 타고 들어가봤고 아래와 같은 tag를 발견했다

      <form action="http://postcode.map.daum.net/search" id="searchForm" class="form_search" target="_self" method="GET">
        ......
      </form>
    3. form이 존재하는 것을 확인했고 한번 더 GPT에게 form의 기본 action 중 어떤 상황이 history를 증가시키는지 체크했다

      조건history stack 추가 여부설명
      action현재 페이지와 다른 경로YesPUSH 발생
      action현재 경로와 동일No페이지가 리로드되지만, 히스토리 변화 없음
      target="_blank"No새 창에서 열리므로 현재 히스토리에 영향 없음
      JS에서 event.preventDefault() 호출No기본 동작이 막힘, 히스토리 변화 없음
      method="GET" 또는 "POST"Yes (둘 다)기본적으로 둘 다 히스토리 push를 동반함
      form이 submit되면 location.href 변경YesURL이 바뀌면 PUSH
      1. 라이브러리 내부 form property에는 action 존재하며 현재 페이지와 다른 경로가 존재하기 history stack이 증가하는 것을 알 수 있다.

해결 방안

문제가 발생하는 위치를 알았으니 해결 방법은 두 가지라고 봤다.

  1. 라이브러리 대체하기. 즉 집접 작성
    1. 라이브러리를 대체하려면 Kakao API를 활용하여 직접 만들면 된다. 가장 확실한 방법이지만 앞으로 올라올 QA의 양과 시간을 생각하면 바로 적용하기 힘들다.
  2. 라이브러리 내부에서 현상 해결
    1. 위 1번으로 인하여 라이브러리 내부에서 해결할 수 있는지 확인을 했고, 검색하는 순간을 catch할 수 있는 onSearch함수를 제공했다.
    2. 배송지 관련 페이지는 여러 페이지에서 접근을 하며, 이 또한 전역상태를 만드는 내 기준에 부합하지 않아 storage를 활용하였고 남겨 놓지 않아도 되기에 session을 활용했다.
      검색마다 session에 값을 쌓이게 했고, 뒤로가기를 발생할 때 검색한 만큼 숫자를 추가하여 실행을 했다. 또한, 안드로이드 뒤로가기 버튼에도 적용이 필요하여 <CustomGoBack />애도 추가했다

이 QA를 통해서 디버깅을 할 때 라이브러리에 대한 문제점을 한번 체크해주는 것이 좋다라는 것을 배울 수 있었다.

GPT활용을 통한 생선성 증가

작년 이맘때 쯤, 외부 협력사 어플에 회사관련 쇼핑몰을 호스팅하기 위해서 프로젝트를 진행한 적이 있다.
이 때까지만 해도, GPT의 능력이 좋지 않았던 것 같고, 결제를 하지 않고 무료로 쓰고 있기에 더 좋다고 느껴지지 않았을 수도 있다.

이번에는 유료버전의 GPT랑 같이 설계도 하고, 코드도 만들고, 리팩터링까지 같이 하면서 이제는 GPT의 능력이 실감이 된다. HTTP의 정책이라든지, 특정 기능을 위한 CSS작성이라든지, 리팩터링에서 많은 부분 물어봤고 좋은 답변과 바로 적용가능한 코드까지도 반환을 헀다.
프로젝트 중반부터 Cursor를 사용했지만, Cursor에게 조건부로 렌더링 로직을 맞겨서 에러가 난 횟수보다 GPT가 더 깔끔하게 처리가 되는 것도 느꼈다.

지금까지 사내서비스만 만들다가 B2C 제품을 만들면서 AI를 써본 결과, AI의 발전으로 쉽게 일처리가 되는 것을 이번 기회에 상당히 체감을 했다. 그러나, 무조건 사용하기에는 아직 부족했다. 내가 알고 있는 지식을 통해서 답변을 한번 더 확인하고 재검증을 해야 좋은 코드가 나왔다. 주변 사람 또는 글에서 AI를 왜 비서처럼 쓰라는 지 단번에 이해할 수 있었다.

profile
꾸준히 공부하자

0개의 댓글