React.js react router dom data fetching

강정우·2023년 1월 31일
0

react.js

목록 보기
39/45
post-thumbnail
post-custom-banner

문제점

  • 그동안 우리가 작성해온 방식은 조금 convention한 코드였다.
    이런 코드의 단점은
  1. 작성할 코드가 제법 많고 자체적으로 관리하기가 복잡하다
  2. 앱이 커지면 코드도 더 복잡해진다.
  3. 컴포넌트로 이동을 시작하자마자 데이터를 가져오는 게 아니라 컴포넌트가 로드된 다음에야 데이터 fetch를 시작한다
  4. 로딩 state와 error state도 Manual하게 관리해야 한다.

예제코드 일부 발췌

  const [error, setError] = useState();
  const [posts, setPosts] = useState();
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    async function loadPosts() {
      setIsLoading(true);
      try {
        const posts = await getPosts();
        setPosts(posts);
      } catch (err) {
        setError(err.message);
      }
      setIsLoading(false);
    }

    loadPosts();
  }, []);

그래서 우리는 react-router-dom 6.4 이상의 버젼을 눈여겨 볼 필요가 있다.

data fetching

loader prop, useLoaderData hook

import 컴포넌트, {loader as 로더함수} from "path"
<Route index element={<컴포넌트 />} loader={로더함수}/>
const 컴포넌트함수 => () => {
  const loaderData = useLoaderData();
  return(
    <>
      <컴포넌트 프롭={loaderData}/>
    </>
  )
}
export default 컴포넌트함수;

export function 로더함수({params}){
  return 패치함수(parms.키값);
};
  • 로더함수는 컴포넌트 함수에서 사용할 수 있는 데이터를 반환할 것이다. 그렇게 코드를 짰으니까

  • 이때 배열이나 객체일 수도 있고 텍스트나 숫자일 수도 있다
    어떤 형태이든 위 함수에서 사용하는 데이터를 리졸브(resolve)하는 프로미스여야 한다.

  • 이때 프로미스객체는 가져오기에 실패하면 사용자 지정 오류 객체를 발생시키고 성공할 경우에는 JSON 데이터를 리졸브하는 프로미스를 응답의 일부로 반환한다.

  • 객체는 페이지 전환 관련 데이터를 포함한 request 객체이거나 params 객체일 수 있다.
    params 객체 뒤에 URL의 path값을 붙이면 URL을 통해 게시물 id에 액세스하게 만들 수 있다.

  • 이때 우리는 로딩 상태는 걱정할 필요 없다 백그라운드에서 데이터를 가져온 후에만 페이지를 로드하니까

RouterProvider, createBrowserRouter, createRoutesFromElements

  • 얘전에 쓰던 <BrowerRouter> 말고 다른 컴포넌트를 사용하는데
const router = createBrowserRouter([
  {path:"" ,element:<컴포넌트/>},
  {path:"" ,element:<컴포넌트/>},
  {path:"" ,element:<컴포넌트/>},
]);
  • 이런식을 객체를 배열화하여 생성할 수도 있고
const router = createBrowserRouter(createRoutesFromElements(
    <Route path={"/"} element={<RootLayout/>}>
      <Route index element={<WelcomePage />} />
      <Route path="/blog" element={<BlogLayout />}>
        <Route index element={<BlogPostsPage />} loader={blogPostsLoader}/>
        <Route path=":id" element={<PostDetailPage />} />
      </Route>
      <Route path="/blog/new" element={<NewPostPage />} />
    </Route>
));
  • 기존에 사용하던 방식으로도 가능하다.
  • 하지만 유의해야할 점은 Routes로 묶는 것이 아닌 하나의 거대한 Route가 되는 것이고 통상 부모 Route는 "/"으로 초기화시켜주면 좋다.

    index prop

  • 또한 index props는 startwith 컴포넌트를 만났을 때 내부 자식 컴포넌트 중 index props을 갖고있다는 해당 컴포넌트를 기본적으로 먼저 보여준다.

    <Outlet>

  • 주의할 점은 위 예제코드에서 root Route인 RootLayout 컴포넌트네에 <Outlet\> 컴포넌트를 넣어 잡아주어야한다는 점이다.

오류처리

errorElemnt prop, useRouteError hook

  • 앞서 Route 태그에서 loader prop으로 로딩의 일련의 과정을 처리하였다. 그럼 error는 어떻게 처리할까? 아주 편리하게도 이것또한 props이 있다.
<Route path={"/"} element={<RootLayout/>} errorElement={<ErrorPage/>}>
  <Route index element={<WelcomePage />} />
  <Route path="/blog" element={<BlogLayout />}>
    <Route index element={<BlogPostsPage />} loader={blogPostsLoader}/>
    <Route path=":id" element={<PostDetailPage/>} loader={blogPostLoader}/>
  </Route>
  <Route path="/blog/new" element={<NewPostPage />} />
</Route>
  • 각 Route 마다 errorElent를 다르게 부여하여 따로 error를 관리할 수 있지만 부모 Route에 하나의 error를 넣어 밑에 자식 컴포넌트까지 관리가 가능하다.
throw { message: 'Failed to fetch post.', status: 500 };
  • useLoaderData 훅을 사용해 loader가 가져온 데이터에 액세스한 것과 마찬가지로
    useRouteError 훅을 사용해 발생한 error에 액세스할 수 있다
  • 이때 어떤 async 코드에서 위와같은 error객체를 생성하였다면
const error = useRouteError();

return (<p>error.messages</p>)
  • 이런식으로 에러 메시지에 접근할 수 있다.

data 제출

예전코드

async function submitHandler(event) {
  event.preventDefault();
  setIsSubmitting(true);
  try {
    const formData = new FormData(event.target);
    const post = {
      title: formData.get('title'),
      body: formData.get('post-text'),
    };
    await savePost(post);
    navigate('/');
  } catch (err) {
    setError(err);
  }
  setIsSubmitting(false);
}

Form

  • 예전엔 양식을 기본 html 태그중 하나인 form태그를 사용하여 sumit을 event.prevent하여 사용했지만 react-router-dom의 도움을 받으면 간단하게 Form 태그로 관리가능하다.
  • props은 method로 우리가 자주 쓰면 http method를 넣어주고 action에는 Route 경로를 넣어주면 된다.
  • 일단 위 코드는 실제로 전송이 되진 않는다. React Router는 클라이언트 사이드 라이브러리라서 대신 action prop을 사용했다.
<Route path="/blog/new" element={<NewPostPage />} action={newPostAction}/>
export async function action({request}) {			// 1.
    const formData = await request.formData();			// 2.
    const post = {			// 3.
        title: formData.get("title"),
        body: formData.get("post-text")
    };
    try {
        savePost(post);			// 4. 
    } catch (err) {
        if (err.status === 422) {
            return err;			// 6.
        }
        throw err;
    }
    return redirect("/blog");			// 5.
};
  1. 위 사진의 Form이 제출될 때마다 이 action 함수가 실행되며 Form data를 포함하는 객체가 자동으로 생성된다
    여기서도 params 객체나 request 객체를 얻을 수도 있는데 요청 객체가 더 중요하다.
    왜냐하면 자동으로 생성된 이 요청 객체는 아직 브라우저를 떠나지 않았기 때문이다.

    • 일반적으로 하듯이 ref나 양방향 바인딩을 사용해 양식에서 수동으로 데이터를 추출할 필요가 없다.
      자동으로 생성된 reqeust 객체의 일부이다.
    • 대신 input 또는 text-area 태그의 name 속성과 비동기를 이용하면 된다.
  2. 이때 또 중요한 것은 request 객체에서 formData 메서드를 실행하면 프로미스가 반환되는데 이를 대기(Await)해야한다.

    • formData 함수를 사용하는 이유는 생성된 기본 요청 객체가 내장 formData 메서드를 갖고 있어 form data에 access를 허용하기 때문이다.
  3. 여기서 title과 body는 반드시 작성요소는 아니고 단순히 savePost 매서드이 유효성 검사 코드를 그렇게 짰기 때문이다.

  4. savePost도 프로미스를 반환하므로 await을 붙여 주고 오류가 발생할 수 있으니 try-catch 블록으로 감싸 준다.

  5. 전에는 React Router가 기본적으로 제공하는 navigate 함수를 호출했었다.
    하지만 redirect 함수도 React Router에서 가져온 또 다른 함수로 다른 페이지로 이동하도록 브라우저를 트리거하는 응답을 생성한다.

    • 이동할 페이지의 경로를 정의해 주면 action을 트리거한 후 쉽게 사용자를 리디렉션한다.
  6. 여기서 throw err가 아닌 return err를 해줌으로써 다시 상위 컴포넌트로 넘어가는 것이 아닌 현재 컴포넌트에 머물면서 useActionData 훅을 사용해 반환된 데이터에 액세스할 수 있다.
    그래서 작은 경고 문구같은 것을 통한 유효성 검사를 구현할 수 있다.

useNavigation

  • useNavigation 훅을 사용하여 제출 중 버튼 비활성화기능을 구현할 수도 있다.
  • 이동(Navigation) 정보 일부를 활용하게 해 주는 객체를 제공하는 훅이다.
    정확히 말하자면 해당 객체의 state 프로퍼티에 액세스하게 해 준다. state에는 idle, loading submitting이 있다.
  • 즉, useNavigation은 action 함수나 loader 함수가 현재 수행 중인 작업에 대한 정보를 제공한다.
  • 그리고 당연하게도 해당 컴포넌트(여기서는 NewPostForm)에 state에 따라 button disabled가 구현이 되어있어야한다.

loading state 알려주기

  • 사용자는 만약 많은 양의 data를 가져올 때 로딩화면을 너무 오래기다려야한다는 단점이 있다. 이를 방지하고자 로드 되는대로 가져와주는 함수가 있다.
function DeferredBlogPostsPage() {
    const loaderData = useLoaderData();

    return (
        <>
            <h1>Our Blog Posts</h1>
            <Suspense fallback={<p>Loading...</p>}>			// 3.
                <Await			
                    resolve={loaderData.posts}			// 2.
                    errorElement={<p>Error loading blog posts.</p>}
                >
                    {(loadedPosts) => <Posts blogPosts={loadedPosts} />}			// 4.
                </Await>
            </Suspense>
        </>
    );
}

export default DeferredBlogPostsPage;

export async function loader() {
    return defer({posts: getSlowPosts(),:함수(), ...});			//1 .
}
  1. 이렇게 defer 함수로 감싸서 객체 형태로 삽입한 다음 이를 posts 키의 값으로 할당하면 된다. 이 객체에서 posts 키에 저장되는 값은 getSlowPosts를 호출한 결과이다
    • getSlowPosts는 프로미스를 반환하는데 왜 await을 안 사용하냐? 여기에는 await이 안 붙는 이유는 당연하게도 await을 갈기면 이게 로드가 될 때 까지 기다리기 때문이다.
    • 그래서 중요한 데이터는 awa로드될 때까지 기다려야 할 중요한 데이터는 대기(await)하고 다른 데이터는 대기하지 않도록 설정할 수 있다
  2. Await 컴포넌트는 이런 식으로 렌더링되며 resolve라는 특수 프로퍼티를 갖는다
    resolve 프로퍼티에는 postphone할 로딩 함수를 가리키는 포인터를 전달한다
    • 바로 getSlowPosts를 Await에서 리졸브하기 위해 loaderData에 포인터를 추가하고 대기하고 있는 것이다
  3. React가 제공하는 Suspense 컴포넌트로 Await를 감싸야 작동한다.
    • Suspense에 fallback 프로퍼티를 추가하고 '로딩 중...'이라는 문구나 로딩 아이콘을 삽입해도 되고 로딩 폴백 코드를 넣어도 된다.
  4. render 프로퍼티 개념을 활용해 작업할 수 있다
    1) Await의 여는 태그와 닫는 태그 사이에 동적 코드를 삽입한 다음 여기에 함수를 입력한다
    2) loadedPosts를 받는 이 함수는 게시물이 로드된 다음에 React Router에 의해 실행된다.
    3) getSlowPosts 함수가 종료된다.
    4) Posts 컴포넌트를 렌더링한다.

useFetcher 훅

  • 기본적으로 양식 제출을 수동으로 트리거할 때 사용한다. => 양식 전송 후 화면 전환을 하고 싶지 않을 때
  • 컴포넌트 내부에서 loader를 수동으로 트리거할 때도 사용한다.
  • request가 백그라운드에서 전송되며 request을 전송할 구체적인 페이지를 대상으로 지정할 수 있다.
function signupForNewsletterHandler(event) {
        event.preventDefault();
        const enteredEmail = emailEl.current.value;
        // could validate input here
        fetcher.submit(
            // better: use fetcher.Form instead
            { email: enteredEmail },
            { method: 'post', action: '/newsletter' }
        );
    }

    return (
        <section className={classes.newsletter}>
            <h2>Sign up for our weekly newsletter</h2>
            <form onSubmit={signupForNewsletterHandler}>
                <input
                    ref={emailEl}
                    id="email"
                    type="email"
                    placeholder="Your email"
                    aria-label="Your email address."
                />
                <button>Sign Up</button>
            </form>
        </section>
    );

결과

  • 수동으로 데이터 페칭을 초기화할 필요도 없고 useEffect를 사용할 필요도 없다
  • 페이지 컴포넌트는 데이터가 있을 때만 렌더링되므로 로딩 state 관리도 필요 없다
  • 즉, 데이터 페칭과 데이터 제출에 관련된 기능이며 관련된 로직을 컴포넌트 외부로 빼내어 컴포넌트를 간결하게 만들어 준다
    평소 쓰던 로직은 간소화된 형태로 action 함수와 loader 함수에 삽입해 줄 수 있다. 다시 말해 형태가 매우 간소화되었다
  • 하지만 경우에 따라서 데이터가 없을 때에도 방문하고 싶을 텐데 그땐
profile
智(지)! 德(덕)! 體(체)!
post-custom-banner

0개의 댓글