최근에는 거의 항상 Tailwindcss만 써서 직접적으로 css 를 import 해서 사용하지 않았기 때문에 다르게 쓰는 방식을 몰라 헤매었다.

SCSS Module Import Error In TypeScript 

에러가 발생하였는데, 계속 잡히지 않아서 이거저거 또 검색해보았다.

Perplexity, Cursor 에서도 제대로 에러를 찾아내지 못했고, 인터넷서칭으로 알아내었다.

 

React.ts 를 이용해서 개발중이었는데,

 

1. @import  대신 @use 를 사용

vite.config.ts

원래는 additionalData 에 @import 값을 넣어서 사용했는데, 2021년부터 @import 대신 @use 를 쓰도록 권장했다고 한다..

 

2. 추가적으로 사용하는 @forward

@use 를 써서 내용에 삽입하여 사용

 

@use 를 위에 써주고 @forward 를 써서 redirect 해준다.

이렇게 사용하면 해당 파일만 index에서 import 해주어도 전역에서 사용이 가능하다. 

다른 경로의 파일에서 사용한 예시

 

 

sass, import, use - ai generated image

반응형

안녕하세요 상훈입니다.

이번 포스팅에서는 React.js 의 라이브러리 중 하나인 React-Query 를 사용해보려고 합니다.

소스를 구현하고 동작시켜보는데, tailwindcss + cursor ai 의 많은 도움을 받았답니다..

일단 결과물 먼저 보고 오시죠!

Result

 

1. React-Query 설치

당연하게 npm i react-query 했는데, 오류나면서 안된다길래. 뭐지..? 싶어서 열심히 검색해왔다.

"@tanstack/react-query": "^5.67.2" //해당 버전을 다운로드 받으면 된다.

//기존의 react-query는 삭제해준다.
npm uninstall react-query 

//tanstack의 버전으로 다운로드 해준다.
npm install @tanstack/react-query

https://tanstack.com/

 

TanStack | High Quality Open-Source Software for Web Developers

Headless, type-safe, powerful utilities for complex workflows like Data Management, Data Visualization, Charts, Tables, and UI Components.

tanstack.com

이곳에서 이제 다운로드 받을 수 있다고 합니다.

 

 

2. Main.tsx

본인 프로젝트의 기본 경로의 파일에서 QueryClientProvider 로 Client 를 제공해줍니다.

ContextAPI와 유사하네요

QueryClientProvider

<React.StrictMode>
    <QueryClientProvider client={queryClient}>
        <BrowserRouter>
            <Header />
            <App />
        </BrowserRouter>
    </QueryClientProvider>
</React.StrictMode>

 

3. 본격적으로 React-Query 를 사용해보자

소스 코드 전체 첨부(최하단에 있어요)

3-1. API's

rest api 형태로 mockapi 데이터 조회,수정,삭제 하는 로직

전역변수로 api 상수를 선언하고 그 안에 [조회, 수정, 삭제] 가 가능한 로직을 만들었습니다.

각각 GET, PUT(UPDATE), DELETE 의 method 를 가지고 있습니다.

 

3-2. 메인 로직 - useQuery

    import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
    //...
    const api = {
        fetchPosts: async ({ pageParam = 1, searchTerm = "" }) => {
            const response = await fetch(
                `${POSTS_URL}?page=${pageParam}&limit=10&search=${searchTerm}`
            );
            return response.status === 200 ? response.json() : [];
    	},
    //...
    }
    
    //...
    const [currentPage, setCurrentPage] = useState(1);
    const [searchTerm, setSearchTerm] = useState("");
    const [debouncedSearch, setDebouncedSearch] = useState("");
    const queryClient = useQueryClient();

    // 쿼리 키
    const postsQueryKey = ["posts", currentPage, debouncedSearch];

    // 데이터 조회 쿼리
    const {
        data: posts = [],
        isLoading,
        error,
    } = useQuery({
        queryKey: postsQueryKey,
        queryFn: () =>
            api.fetchPosts({
                pageParam: currentPage, //페이징처리
                searchTerm: debouncedSearch, //검색기능
            }),
    });

중간에 이것저것 있지만, 다 걷어내고 난 후에 메인 기능은 위와 같다.

postQueryKey 의 요소들이 결국 useQuery의 의존성배열이 되는 것인데,
마치 useEffect의 2번째 파라미터로 넘기는 의존성배열과 같다.

1) "posts"의 테이블로 데이터 요청을 할 것이다.
2) currentPage 의 값이 변경될 때마다 요청할 것이다.
3) debouncedSearch 의 값이 변경될 때마다 요청할 것이다.

 

결과는 [ posts, isLoading, error ] 의 값을 템플릿 리터럴으로 html 코드에서도 사용할 수 있다.

isLoading, error 의 값 활용

1) isLoading: 로딩바 구현
2) error: 에러 시 table 목록을 보여주지 않고 위 텍스트만 노출.
3) posts 를 반복하여 tr 태그에 값을 내려준다.

 

3-3. useMutation

mutation 의 경우는 3단계로 나뉘어진다.

1) useMutation 선언
2) createMutation 호출
3) invalidateQueries 호출

이렇게 글로 정리하니까 되게 간단해보이지만 실제로 써놓은 코드 보면 헷갈릴 수 있다.

 // 뮤테이션 공통 성공 핸들러
const handleMutationSuccess = () => {
    queryClient.invalidateQueries({ queryKey: postsQueryKey });
};

// 뮤테이션 생성 헬퍼 함수에 제네릭 타입 추가
const createMutation = (mutationFn) => {
    const mutation = useMutation({
        mutationFn,
        onSuccess: handleMutationSuccess,
    });
    return mutation;
};

// 업데이트와 삭제 뮤테이션
const updateMutation = createMutation(api.updatePost);
const deleteMutation = createMutation(api.deletePost);

return (
    {/* 사이드바 */}
    {selectedPost && (
        <SideBar
            post={selectedPost}
            onClose={closeSideBar}
            onUpdate={(updatedPost) => {
                updateMutation.mutate(updatedPost);
                closeSideBar();
            }}
            onDelete={(postId) => {
                deleteMutation.mutate(postId);
                closeSideBar();
            }}
        />
    )}


)

수정

Sidebar 에서 정보를 업데이트하여 onUpdate 가 호출 > updateMutation.mutate(updatedPost) 호출 > useMutation(updatePost) 호출 > [실제쿼리동작] > queryClient.invalidateQueries 호출하여 리스트 재조회 

- 캐싱된 내용은 그대로 있고, 캐싱되지 않은 내용만 새로 업데이트 된다.

삭제 

마찬가지로 Sidebar 에서 정보를 삭제 시 onDelete 호출 > deleteMutation.mutate(postId) 호출 > useMutation(deletePost) 호출 > [실제쿼리동작] > queryClient.invalidateQueries 호출하여 리스트 재조회

- 캐싱 동일 적용

 


최대한 요약해봤지만 결국 장황하게 쓰게되었다.

하지만 결론은 생각보다 간단하다

1. 최대한 re-rendering 발생하지 않도록 caching 처리
2. 리스트 기준으로 결국에는 list 재조회를 한다.


 공식문서에 TypeScript 관련하여 useQuery 부분에 대해 선언해놓은게 있는데, <T...>  다 제너릭으로 설정할거면 뭐하러 Typescript로 한거지..? 라는 생각이 든다.

typescript with useQuery

 


**3). UseReactQuery.tsx 소스코드 전체

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useState, useEffect } from "react";
import { ClipLoader } from "react-spinners";
import { PostTableRow } from "../components/PostTableRow";
import SideBar from "../components/SideBar";
import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

const POSTS_URL = "https://mockapi.io/" //이곳에서 프로젝트 생성하시고 랜덤 데이터 생성할 수 있어요!
type POST_TYPE = {
    id: string;
    createdAt: string;
    writer: string;
    title: string;
    avatar: string;
    contents: string;
};

export const IsLoading = () => {
    return (
        <div className="flex justify-center items-center h-screen">
            <ClipLoader color="#4A90E2" size={50} />
        </div>
    );
};

// API 함수들
const api = {
    fetchPosts: async ({ pageParam = 1, searchTerm = "" }) => {
        const response = await fetch(
            `${POSTS_URL}?page=${pageParam}&limit=10&search=${searchTerm}`
        );
        return response.status === 200 ? response.json() : [];
    },

    updatePost: async (updatedPost: POST_TYPE) => {
        const response = await fetch(`${POSTS_URL}/${updatedPost.id}`, {
            method: "PUT",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(updatedPost),
        });
        if (response.status === 200) {
            toast.success("게시물이 성공적으로 업데이트되었습니다.");
            return true;
        } else {
            toast.error("게시물 업데이트에 실패했습니다.");
            return false;
        }
    },

    deletePost: async (postId: string) => {
        const response = await fetch(`${POSTS_URL}/${postId}`, {
            method: "DELETE",
        });
        if (response.status === 200) {
            toast.success("게시물이 성공적으로 삭제되었습니다.");
            return true;
        } else {
            toast.error("게시물 삭제에 실패했습니다.");
            return false;
        }
    },
};

// 페이지네이션 컴포넌트
const Pagination = ({
    currentPage,
    setCurrentPage,
    hasMore,
}: {
    currentPage: number;
    setCurrentPage: (page: number) => void;
    hasMore: boolean;
}) => (
    <div className="mt-4 flex justify-center gap-2">
        <button
            onClick={() => setCurrentPage(Math.max(currentPage - 1, 1))}
            disabled={currentPage === 1}
            className="px-4 py-2 bg-gray-200 rounded disabled:opacity-50"
        >
            이전
        </button>
        <span className="px-4 py-2">페이지 {currentPage}</span>
        <button
            onClick={() => setCurrentPage(currentPage + 1)}
            disabled={!hasMore}
            className="px-4 py-2 bg-gray-200 rounded disabled:opacity-50"
        >
            다음
        </button>
    </div>
);

// 메인 컴포넌트
const UseReactQuery = () => {
    const [currentPage, setCurrentPage] = useState(1);
    const [searchTerm, setSearchTerm] = useState("");
    const [debouncedSearch, setDebouncedSearch] = useState("");
    const [selectedPost, setSelectedPost] = useState<POST_TYPE | null>(null);
    const queryClient = useQueryClient();

    // 디바운스 처리
    useEffect(() => {
        const timer = setTimeout(() => {
            setDebouncedSearch(searchTerm);
            setCurrentPage(1); // 검색어 변경시 첫 페이지로 이동
        }, 500);

        return () => clearTimeout(timer);
    }, [searchTerm]);

    // 쿼리 키
    const postsQueryKey = ["posts", currentPage, debouncedSearch];

    // 데이터 조회 쿼리
    const {
        data: posts = [],
        isLoading,
        error,
    } = useQuery({
        queryKey: postsQueryKey,
        queryFn: () =>
            api.fetchPosts({
                pageParam: currentPage,
                searchTerm: debouncedSearch,
            }),
    });

    // 뮤테이션 공통 성공 핸들러
    const handleMutationSuccess = () => {
        queryClient.invalidateQueries({ queryKey: postsQueryKey });
    };

    // 뮤테이션 생성 헬퍼 함수에 제네릭 타입 추가
    const createMutation = <TData, TVariables>(
        mutationFn: (variables: TVariables) => Promise<TData>
    ) => {
        // eslint-disable-next-line react-hooks/rules-of-hooks
        const mutation = useMutation({
            mutationFn,
            onSuccess: handleMutationSuccess,
        });
        return mutation;
    };

    // 업데이트와 삭제 뮤테이션
    const updateMutation = createMutation<boolean, POST_TYPE>(api.updatePost);
    const deleteMutation = createMutation<boolean, string>(api.deletePost);

    // 사이드바 관련 함수
    const openSideBar = (post: POST_TYPE) => setSelectedPost(post);
    const closeSideBar = () => setSelectedPost(null);

    return (
        <div className="mt-6 relative">
            <ToastContainer />
            <h1>UseReactQuery</h1>

            {/* 검색 필터 */}
            <div className="mb-4 flex flex-col gap-2">
                <div className="flex justify-end">
                    <input
                        type="text"
                        value={searchTerm}
                        onChange={(e) => setSearchTerm(e.target.value)}
                        placeholder="제목 또는 내용으로 검색..."
                        className="px-4 py-2 border rounded-md w-64"
                    />
                </div>
                <div className="h-6">
                    {searchTerm && (
                        <p className="text-sm text-gray-500 text-right">
                            총 {posts.length}개의 결과가 있습니다.
                        </p>
                    )}
                </div>
            </div>

            {/* 상태별 표시 */}
            {isLoading && <IsLoading />}
            {error && <div>에러가 발생했습니다!</div>}

            {/* 데이터 테이블 */}
            {!isLoading && !error && (
                <div className="overflow-x-auto w-screen flex flex-col">
                    <table className="max-w-full table-auto flex-1 text-center mx-4">
                        <thead className="bg-gray-100">
                            <tr className="text-center">
                                <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                    번호
                                </th>
                                <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                    제목
                                </th>
                                <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                    내용
                                </th>
                                <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                    작성자
                                </th>
                                <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                    작성일
                                </th>
                            </tr>
                        </thead>
                        <tbody className="bg-white divide-y divide-gray-200">
                            {posts.length > 0 ? (
                                posts.map((post: POST_TYPE, index: number) => (
                                    <PostTableRow
                                        key={post.id}
                                        post={post}
                                        index={index}
                                        currentPage={currentPage}
                                        onRowClick={openSideBar}
                                    />
                                ))
                            ) : (
                                <tr>
                                    <td
                                        colSpan={5}
                                        className="text-center pt-3"
                                    >
                                        결과가 없습니다.
                                    </td>
                                </tr>
                            )}
                        </tbody>
                    </table>
                </div>
            )}

            {/* 페이지네이션 */}
            {posts.length > 0 && (
                <Pagination
                    currentPage={currentPage}
                    setCurrentPage={setCurrentPage}
                    hasMore={posts.length >= 10}
                />
            )}

            {/* 사이드바 */}
            {selectedPost && (
                <SideBar
                    post={selectedPost}
                    onClose={closeSideBar}
                    onUpdate={(updatedPost) => {
                        updateMutation.mutate(updatedPost);
                        closeSideBar();
                    }}
                    onDelete={(postId) => {
                        deleteMutation.mutate(postId);
                        closeSideBar();
                    }}
                />
            )}
        </div>
    );
};

export default UseReactQuery;

 

이상입니다.

반응형

useContext 를 사용하여 drilling 이 없도록.
Children 컴포넌트에서 제약사항 없이 부모가 내려준 props 를 사용할 수 있도록 하겠습니다.

Result

1. useContext

useContextstore 와 기능이 매우 흡사함.
다만, 작은 규모의 프로젝트 혹은 개인프로젝트에서만 사용하는것을 권장한다.

그 외에는 store 를 사용하자 (redux, zustand ... etc)

그 이유는 아래 내용을 보면 알 수 있다. 가봅시다.

 

2. createContext & Provider

// 1. 테마 컨텍스트 생성
const ThemeContext = createContext();

//TypeScript
const ThemeContext = React.createContext<ThemeContextType | undefined>(
    undefined
);

const UseContext = () => {
    const [theme, setTheme] = useState("light");
    const toggleTheme = () => {
        setTheme(theme === "light" ? "dark" : "light");
    };
    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
            <ThemeComponent_1 />
        </ThemeContext.Provider>
    );
};

처음에 컴포넌트 바깥에서 Context를 생성 (createContext) 해주었다.

그 다음에는 

<ThemeContext.Provider value={{state, action}}>
	<ThemeComponent_1 />
</ThemeContext.Provider>

이렇게 Provider로 감싸서 props로 내려주었다. (여기까지는 일반 Props 와 뭐가다른데? 할 수 있다.)

차이점은 2번째 Drilling 에서부터 나타난다.

 

3. Childrens.

ThemeComponent_1 컴포넌트를 아래와 같이 선언하고 ThemeComponent_2에서 context로 내려준 props 들을 사용할 수 있다.

UseContext > ThemeComponent_1 > ThemeComponent_2 가 되는 꼴이다

//drilling 용 컴포넌트
const ThemeComponent_1 = () => {
    return <ThemeComponent_2 />;
};

//실제 context를 사용하는 컴포넌트
const ThemeComponent_2 = () => {
    const { theme, toggleTheme } = useContext(ThemeContext);

    return (
        <div
            className={`w-full mx-auto px-4 py-8 min-h-screen transition-all duration-300 ${
                theme === "light" ? "bg-gray-50" : "bg-gray-900 text-white"
            }`}
        >
            <h1>현재 테마: {theme}</h1>
            <button onClick={toggleTheme}>테마 변경</button>
        </div>
    );
};

[테마 변경] 버튼을 클릭 하면 light <> dark 로 전환이 되면서 배경색이 변경되도록 수정하였다.

전역적으로 사용하려면  App.jsx 혹은 Index.jsx 에 선언해놓고 Header.jsx 에서 사용하면 될 것이다.

 

Epiloge

확실히 useContext 를 통하여 사용할 수 있는 부분이 눈에 보인다. (가령 테마라던가...테마라던가...테마라던가.)

하지만 규모가 조금이라도 커지면 useContext 를 사용할 수 없을 것 같다. 
유지보수 측면에서 너무 큰 비용이 들어갈 것으로 보임.

그래서 결국에는 store (Redux, Zustand) 를 사용하게 될 것 같다.

 

전체 코드

TypeScript 로 작성해서 간단하게 type 을 선언하여 사용하였다.

import React, { useContext, useState } from "react";
import { FaSun, FaMoon } from "react-icons/fa";

interface ThemeContextType {
    theme: string;
    toggleTheme: () => void;
}

// 1. 테마 컨텍스트 생성
const ThemeContext = React.createContext<ThemeContextType | undefined>(
    undefined
);

const UseContext = () => {
    const [theme, setTheme] = useState("light");
    const toggleTheme = () => {
        setTheme(theme === "light" ? "dark" : "light");
    };
    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
            <ThemeComponent_1 />
        </ThemeContext.Provider>
    );
};

//drilling 용 컴포넌트
const ThemeComponent_1 = () => {
    return <ThemeComponent_2 />;
};

//실제 context를 사용하는 컴포넌트
const ThemeComponent_2 = () => {
    const context = useContext(ThemeContext);
    if (!context)
        throw new Error("ThemeContext must be used within ThemeProvider");

    const { theme, toggleTheme } = context;

    return (
        <div
            className={`w-full mx-auto px-4 py-8 min-h-screen transition-all duration-300 ${
                theme === "light" ? "bg-gray-50" : "bg-gray-900 text-white"
            }`}
        >
            <button
                onClick={toggleTheme}
                className={`p-2 rounded-full ${
                    theme === "light"
                        ? "bg-gray-200 hover:bg-gray-300"
                        : "bg-gray-700 hover:bg-gray-600"
                } transition-colors duration-200`}
                aria-label="테마 변경"
            >
                {theme === "light" ? (
                    <FaMoon className="w-5 h-5 text-gray-700" />
                ) : (
                    <FaSun className="w-5 h-5 text-yellow-300" />
                )}
            </button>
        </div>
    );
};

export default UseContext;

 

반응형

React.js 에서 useTransitionuseDeferredValue 를 몰랐을 때에는 그냥 useEffect 혹은 useMemo 를 사용하여 state를 관리했었는데, 공식홈페이지를 보다가 조금 더 알게된 내용을 공부하고 포스팅한다.

Output

0. useTransition 이란?

- 일반적인 상태 업데이트를 하는데 유용한 React Hook.

useFormStatus 와 유사하게 사용할 수 있다.
( useTransition: 일반적인 상태 업데이트, useFormStatus: Form 제출 상태를 관리)

const {isPending, startTransition} = useTransition()

이런 선언으로 간단하게 시작할 수 있다.

 

일반적인 textInput 값을 사용해보자

 

1. startTransition 으로 query의 값을 변경할 때 setQuery 호출

const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
    startTransition(() => {
        setQuery(e.target.value);
    });
};

//컴포넌트 내용 
return (
	<input
        type="text"
        onChange={handleChange}
    />	
)

1) useTransition은 상태 업데이트를 긴급하지 않은 작업으로 처리한다.
2) isPending 상태를 통해 전환 중임을 사용자에게 표시할 수 있다.
3) React는 더 중요한 업데이트(예: 사용자 입력)를 먼저 처리한 후 이 업데이트를 실행한다.

> Lazy Loading 과 같이 지연 연산으로 처리 한다.
> 만약 onChange 가 한 번 더 호출되면, 이전 연산하던 것은 버리고 새로 연산을 시작 한다.

 

2. defferredValue 선언

const defferredValue = useDeferredValue(query);

위에서 setQuery(newValue) 를 실행할때의 조건을 설정한다고 생각하면 된다.

1번과 마찬가지로 useDeferredValue는 값의 변경을 지연시켜 UI 응답성을 유지합니다.
사용자 입력에 즉시 반응하면서도 무거운 렌더링 작업은 나중에 처리할 수 있게 해줍니다.

 

3. 실제 Filter 처리

const list = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`);
const filteredList = useMemo(() => {
    if (defferredValue === "") return list; //공백일때는 초기화

    return list.filter((item) =>
        item.toLowerCase().includes(defferredValue.toLowerCase())
    );
}, [defferredValue]);

실제로 사용할때는 query 값을 사용하는 것이 아니라, deferredValue 값을 사용해야한다. (지연처리를 위함)

추가적으로 useMemo 를 사용하였는데, 변경되지 않은 부분에 대해 불필요한 Re-Rendering 방지합니다.

 

결국, 궁극적 목표는 UI의 최적화이다. (버벅거림을 없애기)

 

전체 코드

const list = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`);
const TransitionExample: React.FC = () => {
    const [query, setQuery] = useState("");
    const [isPending, startTransition] = useTransition();

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        startTransition(() => {
            setQuery(e.target.value);
        });
    };

    const defferredValue = useDeferredValue(query);
    
    const filteredList = useMemo(() => {
        if (defferredValue === "") return list; //공백일때는 초기화

        return list.filter((item) =>
            item.toLowerCase().includes(defferredValue.toLowerCase())
        );
    }, [defferredValue]);
    
    return (
   	<>
        <input
            className="w-full h-10 px-4 py-3 mr-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
            type="text"
            onChange={handleChange}
            placeholder="검색어를 입력하세요..."
        /> 
   
   		<ul className="divide-y divide-gray-200">
            {filteredList.map((item) => (
                <li
                    key={item}
                    className="py-2 px-3 hover:bg-gray-100 transition-colors duration-150 ease-in-out"
                >
                    {item}
                </li>
            ))}
        </ul>
	</>
  )

 

TypeScript 로 연습했기 때문에 위와 같이 React.FC 같은 Type 선언이 추가되어져 있다.

반응형

2025.02.24 - [FrontEnd/React.js] - [React.js] Stream 데이터 fetch로 처리하기 - 2

 

[React.js] Stream 데이터 fetch로 처리하기 - 2

[React.js] Stream 데이터 처리하기 - 1지난 포스팅에 Stream Data 관련해서 글을 작성하였다. 이번 포스팅에서는 해당 내용을 실제로 적용시켜보는 작업을 한다.  How To Decode?그런데 stream 데이터를 가

code-hoon.tistory.com

지난 포스팅에서 Streaming 을 통해서 데이터를 노출하는 것까지 예시를 통해서 완료하였다.

이번에는 ai text 챗. Chatgpt 처럼 중간에 "정지"할 수 있는 기능을 추가하려고 한다.

 

1. AbortController

// 새로운 AbortController 인스턴스 생성
const abortController = new AbortController()

바로 abortController 기능을 사용하는건데, 해당 기능은 node15 이상부터 사용할 수 있으니 저버전의 서버를 사용하고 계신다면 주의 바랍니다. (그 이하를 쓰는거면 얼마나 오래된겁니까..)

이렇게 abrotController 를 선언하였다면, 

// API 요청에 signal 추가
const response = await fetch(`/api/streaming`, {
    method: 'POST',
    headers: Config.headers,
    body: JSON.stringify(requestBody),
    signal: abortController.signal, // AbortController의 signal 추가
})

signal 을 추가하여 처리할 수 있다.

 


결과 맛보기

zustand(store) 을 이용하여 분리되어있는 api 와 화면을 연결하였고,

실제로 사용한 모습은 다음과 같다.

//api.js
const abortController = new AbortController()

const response = await fetch(`api/`, {
    method: 'POST',
    headers: Config.headers,
    body: JSON.stringify(requestBody),
    signal: useStore.getState().abortController.signal,
})

 

React.js 화면

// 검색종료버튼
const handleStop = useCallback(() => {
    const store = useStore.getState()
    store.abortController.abort()
    store.setIsStopped(true)
}, [])

return (
    <button onClick={handleStop}>
        <FaRegStopCircle className='text-lg' /> {/* 정지아이콘 */}
    </button>
)

 

이제 실제로 동작하는 모습은 "개발자도구 -  네트워크 - REST API 명 - 응답"탭에서 볼 수 있는데,

Streaming 응답 데이터 모습

이런식으로 쭉 하단으로 나열되다가 중단을 시키면

데이터가 생성되다가 끊긴 모습

이렇게 결과가 나온다.

그때 화면에서는 아래와 같이 플로팅 버튼이 노출되도록 설정했고, "재생성하기" 버튼을 누르면 기존에 입력했던(송신했던) 정보를 가지고 python에서는 해당 내용을 가지고 다시 응답을 생성하도록 선언 해놓았다.

재생성하기 버튼

 

중요한건 Streaming 데이터를 REST API 를 통해서 호출하고,
이를 받아서 처리하는 과정에 있으며, 중간에 정지(abort) 하는 기능까지 만들었다는 것이다.

 

반응형

안녕하세요 상훈입니다.

 

해당 프로젝트는 Vue.js, React.js 등 SPA 의 공통적인 요소이기 때문에,
SPA 프로젝트를 빌드하여 같은 오류가 난다면 마찬가지로 진행해주셔도 무방합니다.

 

React.js 프로젝트를 build -> nginx server 에 배포하였습니다.

그런데, nginx 서버에서 index.html 파일을 정상적으로 불러왔는데, 새로고침했을 때 404에러가 떠버렸습니다.

왜 404 Not Found 가 뜨냐고!

 

 

기본적인 빌드 환경은 다음과 같습니다.

/var/www/html/build/index.html 

그렇다. build 라는 디렉터리가 또 껴있는 것이다. 

그래서 error.log 를 줄기차게 테스트해보면서 확인해봤다.

이렇게 경로를 읽어오고 있었던 것.

이제 에러를 확인했으니, 고쳐야지..

*참고 : error.log 는 /var/log/niginx/error.log  에 있다.

일단 Ubuntu, Nginx 를 이용하는 입장에서만 서술하도록 하겠습니다.

 /etc/nginx/sites-enabled/default  를 수정하면 되는데, 아래 이미지처럼 수정하면 된다.

server {
	root /var/www/html/build/;
    
    index index.html
    
    #... 이하생략
    
    location / {
    	#이부분을 수정해주시면 됩니다.
    	try_files $uri $uri/ /index.html;
    }   
}

저는 저 try_files 부분에서 index.html 로만 작성이 되어져 있었기에
build/index.html 이 아닌 buildindex.html 로 uri 호출이 되었더라구요.

수정내역

 

그래서 위와 같이 수정해주고, (sudo nano default) 저장. 그리고 서버 재시작

 sudo service nginx restart 

 

그리고.....

감덩...👍

새로고침 시 해당 페이지가 다시 호출되어 화면에 렌더링 되는 것을 볼 수 있습니다.

 

이게 뭐라고 계속 이렇게 끙끙 앓았다니 속상하네요.

반응형

"한빛미디어 <나는 리뷰어다> 활동을 위해서 책을 제공받아 작성된 서평입니다."

안녕하세요 상훈입니다.
이번 달 한빛미디어-나는리뷰어다 에서 제공 받은 도서는 소문난 명강의 : 김범준의 핸즈온 리액트 네이티브 입니다.

리액트에 대한 관심과 공부 시간이 어느 정도 들어간만큼 기초적인 리액트에 대한 개념은 안 상태로 이 도서를 전자책으로 얻게 되어, 작성하였습니다.

 

🙄 어느 정도 수준의 책인가요?

책 소개에서도 언급이 되었지만, 이 도서는 초급 수준 리액트 네이티브 개발을 한다고, 말해주고 있습니다. 

그러나, 찬찬히 살펴면 어느 정도 개발을 하셨던 분들도 보시면 중간중간에 도움이 될 만한 내용들이 많이 있는 것 같습니다. 추천드려요!

 

🎆 목차

간략한 목차부터 말씀드리자면, 이 책은

1. Expo, React-Native, Node.js 등 기초 요소 설명
2. 프로젝트1 : 계산기
3. 프로젝트2 : TodoList
4. 프로젝트3 : 여행 사진 공유 앱 만들기 

로 구성되어져 있습니다.

 

🚩 Prettier, EsLint 

첫 번 째 챕터인 Expo, React-Native, Node.js 등 기초 요소 설명 부분에서 좋았던 부분은 Prettier, EsLint 를 어느정도 이해할 수 있게 설명해준 부분이었습니다.
예전부터 Prettier, EsLint 는 프론트엔드 개발자들을 꽤 속썩이는 역할을 하고 있었거든요. (물론 주관적인 견해입니다.)

저 또한 Prettier, EsLint 때문에 골치 아팠던 적이 한 두 번이 아니었는데, 우연찮게도 이를 어느정도 설명해주어 덕분에 Prettier, EsLint 에 대해 좀 더 알게되었습니다 😎

 

🚩 Expo

그리고 리액트 네이티브 개발 공부를 조금이라도 해본 사람이라면 모두 알고있는 Expo.

이 책 또한 편의성을 중점적으로 Expo를 사용하여 앱 개발의 초석을 만들었습니다.

Expo를 사용하여 첫 App 에뮬레이터를 띄운 상태

덕분에 오랜만에 Expo를 다시 깔게 되었네요.

 

🚩 첫 번째 프로젝트 : 계산기

1. prop-types 라는 라이브러리를 처음 알게되었습니다.

npm i prop-types

TypeScript 처럼 javascript 에서도 타입을 명시하여 사용할 수 있게 해주는 라이브러리더라구요.
매우 유익했습니다!

Button 의 타입정의 사용하기

 

 

그리고 계산기 앱을 한 개 만들어본 결과 느낀점 🚀

책을 받고 시간이 부족해 기간 내로는 클론 코딩이 어려울 것 같아 계산기 앱까지만 만들고, 리뷰를 시작하게 되었다.

React.js 를 사용하던 사람은 React-Native 를 확실히 조금만 더 공부하면 바로 사용할 수 있다.

React-Native 에 해당하는 새로운 컴포넌트 등이 많기에 그렇게 바로바로 사용할 수는 없다.

이 책은 하나하나 상세한 내용을 만들어주고, 전체적으로 오타도 거의 없는 것 같아 좋았다. 여러 개발 관련 도서들은 오타가 워낙 많아 오류도 많이 발생하게 되는데, 이 책은 그런 경우가 없었던 것 같다. (아닐 수도 있음..)

 

⚡ 제가 드리는 별점은요

⭐⭐⭐⭐⭐ 5개 드리겠습니다. 추천드립니다!

리액트 네이티브를 사용하고 싶으신 프론트엔드 개발자라면, 입문서로 봐도 무방하겠다는 생각이 들었습니다.

 

이상입니다.

"한빛미디어 <나는 리뷰어다> 활동을 위해서 책을 제공받아 작성된 서평입니다."

반응형

안녕하세요 상훈입니다.

오늘 제가 구현한 내용은 해외 개발자들에게는 익숙한 Buy me a coffee입니다!

한국에서 제대로 처리되는 내용은 아직 없기 때문에 한국 버전으로 제가 만들어보았습니다.

QR-code 를 이용한 방법입니다.

 

사용환경 🏠

- React.js, TypeScript

 

개발결과 🚀

1. Real Link with Netilfy

2. GithubLink

만약 같은 내용으로 링크, 혹은 텍스트만 변경하고자 하신다면, 2.GithubLink를 방문하셔서 소스를 다운로드 하신후 사용해주세요!

특별한 디자인 요소를 넣지않고 개발해놓았습니다.
간단한 자기 소개 내용으로 처리했습니다. 

- Kakao, Toss 버튼에 따라 각각의 QR코드가 띄워지게 처리했습니다!

- 글을 작성하면서 떠오른 생각인데, 모바일 or 웹을 구분해서 버튼 클릭시 동작이 다르게 처리가 되면 좋겠다고 생각이 들었네요. 
추가 구현이 가능하면 구현하도록 하겠습니다.

 

클릭시 해당 페이지로 이동합니다.

 

 

여유롭게 커피 한 잔 사주시면 감사합니다~

 

반응형

+ Recent posts