코딩/React

Remix Framework 서버사이드 loader, action 함수 알아보기

드리프트 2022. 3. 21. 22:51
728x170
Remix Framework loader, action Function, 리믹스 프레임워크 서버사이드 loader, action 함수

 

 

안녕하세요?

 

지난 시간에 배웠던 리믹스 프레임워크의 다이내믹 라우팅에 더해서 오늘 시간에 배울 부분은 리믹스 프레임워크의 가장 강력하고 편리한 기능이라고 할 수 있는 loader, action 함수입니다.

 

loader, action 함수는 NextJS를 이용해서 취미용 프로젝트를 많이 만들었던 저로서는 리믹스 프레임웍의 서버사이드 함수의 간결하면서 편리한 형식에 감탄을 금치 못했는데요.

 

이제 본론에 들어가 보도록 하겠습니다.

 

먼저, NextJS에서의 서버사이드 코딩인데요.

 

예를 들어 보겠습니다.

 

import { useState, useEffect } from "react";
import { GetServerSideProps } from "next";

const TestPage = (props) => {
  const movies = props.movies;

  return (
    <div>
      <h1>Movies</h1>
      {movies &&
        movies.map((movie: any) => <div key={movie.id}>{movie.title}</div>)}
    </div>
  );
};

export default TestPage;


export const getServerSideProps: GetServerSideProps = async ({
  req,
}): Promise<any> => {

    const response = await fetch(
      `https://api.themoviedb.org/3/movie/popular?api_key=d5c35e51c81488b19da7c1f572507a3d&page=1`
    );

    const data = await response.json();


  return {
    props: {movies: data.results},
  };
};

 

NextJS에서는 서버사이드 함수는 getServerSideProps 함수나, getStaticProps 함수를 통해 구현할 수 있습니다.

 

위 코드를 보면 getServerSideProps 함수에서 fetch 함수를 통해 api.themoviedb.org 사이트의 popular movie 20개를 가져오고 있습니다.

 

그리고 그 정보를 data에 저장하고 다시 props를 리턴하고 있습니다.

 

NextJS에서 리턴하는 props는 plain Object 즉, 간결한 객체여야 합니다.

 

그래서 props: { movies: data.results}라고 써야 합니다.

 

getServerSideProps 함수에서 리턴된 props는 클라이언트 사이드에서 처리되는 React 컴포넌트에서 사용할 수 있습니다.

 

위에서 보시면 TestPage가 클라이언트 사이드 쪽 리액트 클라이언트인데요. props를 인수로 받고 그 데이터를 화면에 뿌려 주고 있습니다.

 

위 코드는 대표적인 NextJS의 서버사이드 코드인데요.

 

이제 Remix Framework에서의 서버사이드 코드를 살펴보겠습니다.

 

리믹스에서는 서버사이드 코드는 특별한 이름의 함수로 실행됩니다.

 

먼저, loader 함수입니다.

 

loader 함수는 페이지가 처음 로드될 때 서버사이드 쪽에서 실행되는 역할을 합니다.

 

그래서 loader 함수를 만들고 export 하면 Remix 프레임웍이 알아서 페이지 첫 로딩 때 loader함수를 먼저 로딩하게 됩니다.

 

loader함수는 클라이언트 상에는 절대 보이지 않는 서버사이드 쪽 코드입니다.

 

export const loader: LoaderFunction = () => {
  return { movies: "Squid Game" };
};

위와 같이 이름만 loader라고 명명하고 export 해주면 리믹스가 알아서 loader함수라고 인식하고 맨 처음 실행하게 됩니다.

 

loader함수에서 return 한 데이터는 클라이언트 사이드 쪽인 리액트 컴포넌트에서 접근할 수 있는데요.

 

위 코드에서는 객체를 리턴했고 movies 항목으로 "Squid Game"이란 string이 지정되어 있습니다.

 

리액트 컴포넌트에서 loader함수에서 리턴한 데이터에 접근하는 방법은 리믹스가 제공하는 useLoaderData 훅을 이용하는 방법이 있습니다.

export default function Movies() {
  const data = useLoaderData();

  return (
    <div>
        {data.movies}
    </div>
  );
}

위 코드에서 useLoaderData 훅을 이용해 loader함수가 리턴하는 객체를 data에 저장하고 그리고 그 data를 UI로 화면에 뿌려 주고 있습니다.

 

참고로 페이지의 메타정보를 지정할 수 있는 meta 함수도 서버사이드 쪽 함수인데요.

 

meta 함수에서도 loader 함수가 리턴하는 데이터를 읽을 수 있습니다.

 

meta 함수의 props에는 data라는 항목이 있는데 props.data가 바로 loader함수가 리턴하는 데이터입니다.

 

그래서 아래와 같이 meta 함수를 정의하면 현재 페이지의 meta 정보를 데이터가 맞게 수정할 수 있는 거죠.

 

export const meta: MetaFunction = (props) => {
  return {
    title: `${props.data.movies} : Popular Movies Nowadays `,
    description: `${props.data.movies} : The Most Popular Movies in TMDB API.`,
  };
};

페이지의 meta 정보인 title이 변경된 걸 보실 수 있을 겁니다.

 

이제 loader 함수를 이용해서 좀 더 멋진 작업을 해볼까요?

 

TMDB API를 이용해서 영화 정보를 불러오도록 하겠습니다.

 

https://api.themoviedb.org/3/movie/popular?api_key=<<api_key>>&language=en-US&page=1

위 코드를 fetch 하면 현재 가장 유명한 영화를 20개 보여줍니다.

 

FireFox를 이용해서 TMDB API 데이터를 불러오기 해봤는데요.

 

일단 우리가 관심 가져야 할 데이터는 바로 위에서 보면 results인데요.

results가 Array 타입으로 우리가 원하는 정보를 담고 있습니다.

 

이제 리믹스에서 TMDB API를 이용해서 데이터를 가져오는 방법을 loader 함수를 통해 구현해 보겠습니다.

 

import {
  LoaderFunction,
  useLoaderData,
} from "remix";


export const loader: LoaderFunction = async () => {
  const response = await fetch(
    `https://api.themoviedb.org/3/movie/popular?api_key=d5c35e51c81488b19da7c1f572507a3d&page=1`
  );
  const data = await response.json();
  
  return data.results;
};

export default function Movies() {
  const movies = useLoaderData();
  console.log("Total returned movies : " + movies.length);

  return (
    <div>
      <h1>Movies</h1>
      <br />
      {movies &&
        movies.map((movie: any) => <div key={movie.id}>{movie.title}</div>)}
    </div>
  );
}

코드를 보여주기 위해서 CSS는 제외했습니다.

 

loader함수에서 fetch 함수를 써서 데이터를 불러오고 json() 함수를 통해 json 객체화시켜서 리턴하면 리액트 컴포넌트에서 useLoaderData 훅을 이용해서 데이터를 얻어오고 있습니다.

 

참고로 app/routes/movies 폴더의 index.tsx파일로 저장했습니다.

 

loader 함수는 서버사이드 함수이기 때문에 loader 함수에서 데이터가 지연될 경우 브라우저 화면에는 아무것도 나타나지 않게 됩니다.

 

왜냐하면 서버사이드 함수가 먼저 실행되고 그다음에 클라이언트 사이드 컴포넌트가 보이기 때문이죠.

 

그래서 loader함수에서 데이터가 미결되는 순간 보여주는.. loading... 문구 같은 게 필요한 데 리믹스에서는 이것도 제공하고 있습니다.

 

나중에 자세히 알아보겠습니다.

 

 

action 함수

 

두 번째로 알아볼 서버사이드 함수는 바로 action 함수인데요.

 

action 이란 이름으로 함수를 만들어서 export 하면 리믹스는 리믹스 프레임웍의 action 함수로 인식하게 됩니다.

 

loader 함수가 데이터를 받아오는 함수였다면 action 함수는 클라이언트 쪽에서 데이터를 받아서 서버사이드 쪽으로 전달하는 함수입니다.

 

예전에 React가 나오기 전, PHP가 유행했을 때는 모든 유저 액션은 form이라는 HTML 태그를 통해 RESTful 하게 이루어졌는데요.

 

form으로 데이터를 create, delete, update 하는 방식이 전통적인 웹 애플리케이션의 소통방식이었습니다.

 

그래서 HTML 코드에서 form을 submit 하게 되면 브라우저는 자동적으로 화면을 갱신하게 되는 거죠.

 

그래서 리액트 코드 작성할 때 다들 event.preventDefault() 함수를 많이들 썼을 겁니다.

 

바로 form 태그의 submit 수행 시 브라우저가 다시 로드되지 않게 하는 명령어였죠.

 

리믹스가 추구하는 코딩 이념이 뭔가 웹의 정통성에 주안점을 두고 있는 거 같습니다.

 

그래서 action 함수를 도입했는데요.

 

리믹스의 action 함수는 클라이언트에서 form 태그를 이용해 데이터를 서버사이드로 넘겨줄 때 그 데이터를 핸들링하는 함수입니다.

 

예를 들어 보겠습니다.

 

우리가 아까 만든 TBDB API에서 search 기능을 추가해 보겠습니다.

 

당연히 search 기능은 input 태그를 통해서 구현하면 또 당연히 submit 하게 됩니다.

 

먼저, UI 부분을 추가해 보겠습니다.

 

export default function Movies() {
  const movies = useLoaderData();
 
  return (
    <div>
      <form>
        <label htmlFor="title">Search Movies :</label>
        <input className="ml-2 border-2" type="text" id="title" name="title" />
        <button type="submit">Search</button>
      </form>
      <h1>Movies</h1>
      <br />
      {movies &&
        movies.map((movie: any) => <div key={movie.id}>{movie.title}</div>)}
    </div>
  );
}

form 태그 안에 input 태그를 잘 보셔야 하는데요.

 

input 태그의 항목 중에 name 항목을 "title"로 지정했습니다.

 

이 부분을 잘 기억해 두시면 됩니다.

 

그럼 action 함수를 만들기 전에 아까 위에서 만들었던 search input을 submit 해볼까요?

 

"red"라고 입력하고 search 버튼을 클릭해 보겠습니다.

 

화면이 리 로딩되면서 뭔가 바뀐 거 같은데 브라우저 내용은 그대로입니다.

 

뭐가 바뀐 걸까요?

 

바로 주소창의 URL입니다.

 

localhost:3000/movies 였는데 search 버튼을 누른 후에 /movies?title=red 라고 url params 가 추가되었습니다.

 

왜 그런 걸까요?

 

이 현상은 바로 HTML의 form 태그의 가장 기본적인 submit 결과입니다.

 

form 태그는 method를 지정할 수 있는데요. 지정하지 않으면 method="get"이 됩니다.

 

form 태그가 method="get"일 경우 바로 input의 name 부분이 url에 추가되고 유저가 입력한 값이 뒤이어 붙여지게 됩니다.

 

그래서 위에서 처럼? title=red가 된 것이죠.

만약에 form 태그의 input 태그에서 name을 t로 바꿔볼까요?

 

그리고 다른 input 태그를 추가해 볼까요?

URL 부분이 추가됐습니다.

 

위에서 살펴본 행태가 바로 웹의 기본 행태인 것이죠.

 

리액트에서는 useState를 이용하고 submit 한 경우 handleSubmit함수를 만들어서 화면을 리프레쉬하지 않고 그 안에서 데이터를 쥐락펴락했다면, 리믹스는 그냥 웹의 순수한 작동방식을 이용해서 작업하는 걸 추구합니다.

 

그러면 action 함수는 언제 쓰일까요?

 

form의 method="get"이 아닐 경우에 사용하면 됩니다.

 

굳이 이럴 필요까지는 없는데요. 편하실 대로 쓰시면 됩니다.

 

일단 우리가 여기서 볼 힌트는 유저가 입력한 정보가 URL을 통해서 보이고 있습니다.

 

그럼 이 URL을 한번 이용해 볼까요?

 

export const loader: LoaderFunction = async ({ request }) => {
  const url = new URL(request.url);
  const title = url.searchParams.get("title");

  const response = await fetch(
    `https://api.themoviedb.org/3/movie/popular?api_key=d5c35e51c81488b19da7c1f572507a3d&page=1`
  );
  const data = await response.json();
  const filteredData = data.results.filter((m: any) =>
    title ? m.title.toLowerCase().includes(title.toLocaleLowerCase()) : true
  );
  return filteredData;
};

loader함수를 살짝 개조했습니다.

 

request 인자를 받아서 url부분을 검사하고 title이란 params을 추출하는 코드입니다.

 

그리고 TMDB API 데이트를 불러온 다음 그걸 title을 이용해서 filter 해주고 리턴해주는 코드입니다.

 

즉, TMDB에서 불러온 20개의 영화 제목에서 유저가 입력한 텍스트가 포함되어 있는 것만 loader 함수가 리턴해 주는 모양새입니다.

 

테스트 결과를 볼까요?

 

title="red"일 경우 영화 제목이 2개만 나옵니다.

 

지금까지 작성하고 이해하셨다면 뭔가 리액트의 useState와 handleSubmit 함수를 이용해서 useEffect함수에서 얻은 데이터를 가공하는 듯한 모습이 상상이 되지 않나요?

 

네 맞습니다. 리액트에서 했던 STATE관리와 uesEffect에서의 데이터 fetch가 모두 웹의 기본적인 기능으로도 충분히 수행할 수 있는 작업인 것이죠.

 

이게 바로 리믹스 프레임웍이 추구하는 본연이라고 개발자가 얘기하고 있습니다.

 

그럼 도대체 action 함수는 언제 쓰일까요?

 

좀 더 어려운 예제로 들어가 보면 만약에 글을 작성해서 form method="post"로 submit 하게 되면 action 함수에서 form 데이터를 이용해서 DB작업을 완료하고 최종적으로 /posts 경로로 redirect 하게 되는 시나리오를 생각하게 될 겁니다.

 

실제, action 함수는 아까 같은 시나리오일 때 쓰는 함수이기 때문이죠

 

그럼, 여기서 응용해서 위에서 만든 영화 리스트를 filter 하는 기능을 action 함수를 통해 구현해 볼까 합니다.

 

먼저, <form method="post">라고 수정해서 action 함수를 이용토록 합시다.

 

export const action: ActionFunction = async ({ request }) => {
  const body = await request.formData();
  console.log("body -->", Object.fromEntries(body));
  const title = body.get("title");

  return redirect("/movies?title=" + title);
};

위와 같이 action 함수는 request 인자를 받고 request의 formData 함수를 이용해서 body 부분을 취하고, 거기서 다시 title 부분을 떼어냅니다.

 

그리고 redirect라는 리믹스 함수를 이용해서 라우팅을 url을 추가해서 돌려주게 되는 거죠.

 

이렇게 되면 아까와 똑같이 loader함수에서 URL부분을 검사해서 title 부분을 filter 한 결과를 내보내게 됩니다.

 

request.formData() 함수나, body.get() 함수나 모두 웹 고유의 기능입니다.

 

추가적인 패키지 없이 웹의 고유 기능을 이용해서 리액트의 State 관리를 멋지게 수행하고 있습니다.

 

최종적인 코드를 보시면 React의 useState, useEffect 함수 하나 쓰지 않고 코드를 작성한걸 눈치채셨을 겁니다.

 

뭔가 리믹스는 웹, 자바스크립트 본연으로 들어가서 코딩한다고 할까요?

 

계속 공부해 보고 싶은 프레임웍이 오래간만에 나왔네요.

 

그럼.

 

그리드형