ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 프로젝트를 다방면으로 생각해보기
    프론트엔드 2026. 6. 14. 11:41

    다시 시작..

     

    프로젝트를 구현하면서 기능을 완성하는 데 집중한 나머지 다양한 관점에서 충분히 고민하지 못했다는 것을 깨달았다.
    구현이 끝나면 ‘이제 다 했다’는 안도감이 들었고 그 때문에 사용성이나 예외 상황 개선 가능성을 놓치기도 했다.
    이번 기회를 통해 구현에서 끝나는 것이 아니라 완성 이후에도 더 나은 방향을 고민하는 습관을 만들어가고자 한다.

     

     

    예외 상황 생각해보기 🤔

    - 현재 상황을 다이어 그램으로 생각해 봤다. 

    - 간단하다 유저가 블로그 글 생성을 할때 나가면 api를 연결해서 데이터를 유지해서 db에 insert할지 아니면 네트워크를 abosrtController로 취소할지에 대한 그림이다. 

     

    AbortController로 진행 중인 저장 요청 취소하기 또는 글 생성 중 유저 이탈 시 안전하게 요청 취소하기

    - 유저가 글 생성 취소할때 연결을 취소 시키는 방법이다. 흐름은 다음과 같다. 

     

    1. 글 생성 완료 → 자동으로 DB 저장 시도 (데이터 유지)
    2. 저장 중 유저가 나가면 → AbortController로 취소 신호 (AbortSignal)
    3. 이미 진행 중인 요청은 최대한 마무리

     

    코드로 흐름을 보면 

    - 아래의 뒤로 가기 버튼이 클릭이 되면 

    import Link from "next/link";
    import { ArrowLeft } from "lucide-react";
    
    export default function GeneratingDraftHeader() {
      return (
        <header className="px-8 py-6 border-b border-navy-700 shrink-0">
          <div className="flex items-center gap-4">
            <Link
              href="/write"  // 단순히 /write로 이동
              className="flex items-center gap-2 text-slate-400 hover:text-white transition-colors">
              <ArrowLeft size={20} />
              <span>뒤로 가기</span>
            </Link>
            <div className="h-6 w-px bg-slate-600" />
            <h1 className="text-xl font-semibold text-white">AI 블로그 생성</h1>
          </div>
        </header>
      );
    }

     

    - 자동으로 저장되도록 구현한 코드를 signal을 통해 신호 응답에 따라 제어를 했다.

    - AbortController를 선언하고 ref를 사용해서 컴포넌트 언마운트시 클린업 함수를 통해 자동으로 취소 되도록 구현했다.

        activeSaveAbortRef.current?.abort(); -> abort()함수가 요청을 취소 시키는 함수다. 

    // 진행 중인 저장 API 호출을 추적하는 ref
    const activeSaveAbortRef = useRef<AbortController | null>(null);
    
    useEffect(() => {
      return () => {
        // 컴포넌트 언마운트 시 자동 취소
        activeSaveAbortRef.current?.abort();
      };
    }, []);
    
    // 자동 저장 시작
    const controller = new AbortController();
    activeSaveAbortRef.current = controller;
    
    const { postId } = await saveGeneratedArticle(
      article,
      payload.selectedTemplate,
      { signal: controller.signal }  // ← 신호 전달
    );

     

    - 다음으로 ai한테 api 요청을 하는 작업을 중단시켜야 한다. 

    useEffect(() => {
      if (!payload) {
        setError("작성 정보가 없습니다.");
        setIsGenerating(false);
        return;
      }
    
      const controller = new AbortController();  // fetch 신호 생성
      
      const run = async () => {
        try {
          const res = await fetch(aiEndpoint(), {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload),
            signal: controller.signal,  // ← fetch 요청에 신호 연결
          });
          
         
          // ... 처리
        } catch (err) {
          if (isAbortError(err) || controller.signal.aborted) return;
          // AbortError는 무시 (정상적인 취소)
          setError(/* 실제 에러만 처리 */);
        }
      };
    
      run();
    
      return () => controller.abort();  // ← 언마운트 시 요청 취소
    }, [payload, templateKey]);

    정리 

    사용자가 "뒤로 가기" 클릭
    ↓
    Next.js에서 /write 페이지로 네비게이션
    ↓
    GeneratingDraft 컴포넌트 언마운트 (리액트 라이프사이클)
    ↓
    useGeneratingDraft의 cleanup 함수 실행
    │
    ├─ activeSaveAbortRef.current?.abort()
    │ └─ saveGeneratedArticle의 fetch 요청 취소
    │
    └─ useArticleGeneration의 cleanup 함수 실행
    └─ controller.abort()
    └─ AI API (Gemini/OpenAI) fetch 요청 취소
    ↓
    모든 진행 중인 요청 중단
    ↓
    상태 업데이트 안 됨 (isAbortError로 필터링)
    ↓
    /write 페이지 렌더링

    - 또한 현재 코드로만 보면 어떤 효과가 있는지 알고싶어 실제 openai 코드의 토큰 사용량을 비교해봤다. 우선 만약 abortController가 없을시에는 기존 토큰에서 600토큰 가량을 낭비하게 되지만, 이를 abortController를 사용하니 800토큰 만큼의 낭비를 줄일 수 있게 되었다. 

     

    2번째 시나리오 유저가 글쓰는 사이트를 나가거나 닫아도 유지해서 보여주기 

    - 유저가 시도할만한 것들을 막아보자 

    1. 유저가 뒤로가기 했을때 데이터 유지 

    2. 유저가 경로를 강제로 쳤을때의 예외처리

     

    유저가 뒤로가기를 한다면??

    - 유저가 뒤로가기를 했을때 고려해볼만한 문제는 아무런 예외처리를 토큰이 낭비하는 문제를 발견했다. 

    - 이를 해결해보고자 세션 스토리지에 상태를 넣어 이를 가져와 상태에 따라 유저가 생성하기 버튼을 클릭해도 값을 재생성하지 못하도록 막아보았다. 

    export function setGenerationStatus(status: GenerationStatus): void {
      if (typeof window === "undefined") return;
      sessionStorage.setItem(WRITE_GENERATION_STATUS_KEY, status);
      notifyGenerationStatusChange();
    }

    - 우선 간단하게 세션 스토리지에 위와 같이 만들어 상태 값을 넣어서 제어를 해보고자 했다. 

    - 저 status의 상태는 크게 3가지로 나눴다.export type GenerationStatus = "generating" | "done" | "error";

    - generating일때 뒤로가기를 눌렀을때 세션 스토리지의 상태 값을 가져와 상태에 따라 경로 리다이렉트와 버튼text를 다르게 보여수정해봤다. 왼쪽 버튼이 스토리지에 값이 없을때이고 스토리지에 generating이면 생성화면이 나오도록 수정했다. 

    스토리지 상태 값

     

    유저가 경로를 강제로 리다이렉트 한다면?

    - 우선 가장 먼저 떠오른 것은 Next.js의 미들웨어로 처리하는 방법이 먼저 떠올랐다. 

    - 현재 Next.js 에서 proxt.ts로 미들웨어 이름이 변경이 되었기 때문에 이에 맞게 수정을 했다. 

    - 우선 기존 세션 스토리지에 값을 넣어놨기때문에 이를 활용해보고자 했다. 

    🤔 여기서 문제점 미들웨어는 세션스토리지에 접근을 못한다... 그래서 이를 해결해보고자 쿠키에 직접 데이터를 넣어 해결해보고자 했다. 
    쿠키는 요청할때마다 서버로 데이터가 직접 가기 떄문에 쿠키를 사용했다. 
    function guardGeneratingPage(req) {
      if (pathname !== "/write/generating") return;  // 다른 경로는 무시
      const hasEntry = req.cookies.get("write-generating-entry")?.value === "1";
      if (!hasEntry) {
        return redirect("/write");   // ❌ 쿠키 없음 → 차단
      }
      const res = next();
      clearGeneratingEntryCookie(res);  // ✅ 통과 후 쿠키 삭제 (1회용)
      return res;
    }

    - 그래서 위오같이 쿠키에 임시 토큰 1을 넣어 이를 통해 유저가 특정 경로로 들어오면 들어오지 못하도록 막았다. 

    ⚡️ 토큰 제어?? 
    버튼 누를 때 쿠키 넣고 → 첫 입장 때 proxy가 확인 후 바로 삭제
    - 위 플로우를 만들면 경로를 다시 재 진입할때 쿠키에 데이터가 없어 미들웨어를 통해 경로 보호가 된다고 생각이 들었고 현재 잘 작동이 된다.

     

    아직 해결해야할 숙제 

    - 새로고침하면 쿠키가 삭제되는데 이를 어떻게 방지할지 좀 생각해봐야겠다

    - 그리고 추가로 세션 스토리지 방식은 보안 적인 측면이나 많은 문제가 생길수 있어 db에 데이터를 저장시켜 이를 보완해보고자 한다. 

     

    결론

    - 항상 한 방법으로만 생각해보지 말자. 프로젝트 개선할 사항은 정말 무궁무진하다는 것을 깨달았다. 

Designed by Tistory.