Next.js 로 미니 무비앱 만들기

seul_velog·2023년 3월 10일
0
post-thumbnail

Layout component

📌 Layouts - next.js 공식
React 모델을 사용하면 페이지를 일련의 컴포넌트로 분해할 수 있다. 이러한 컴포넌트 중 많은 부분이 페이지 간에 재사용되는 경우가 많은데, 예를 들어 모든 페이지에 동일한 navigationfooter 가 있을 수 있다.

_app.js 파일

  • Next.js에서 앱 컴포넌트를 커스터마이징하기 위한 파일이며, 앱 전역에서 사용되는 리소스를 로드하거나 설정할 수 있는 중요한 역할을 한다.

  • _app.js 파일에서는 전역으로 사용되는 CSS, 모든 페이지에서 공통으로 사용되는 컴포넌트, 그리고 추가적인 설정 등을 정의할 수 있다.

  • Google Analytics와 같은 서비스를 사용하기 위한 스크립트나, 검색 엔진 최적화(SEO)를 위한 메타 태그를 추가하거나, 서버사이드 렌더링(SSR)에 필요한 작업을 수행하는 스크립트 등을 구성할 수도 있다.

✍️ 따라서 UI를 구성하고자 할 때 이러한 커다란 react.js component 를 사용하는 대신 Layout component를 활용할 수도 있다는 것 같다. 🤔

import NavBar from './NavBar';

export default function Layout({ children }) {
  return (
    <>
      <NavBar />
      <div>{children}</div>
    </>
  );
}




Head 만들기

📌 Head - next.js 공식

  • Next.js에서 제공하는 head component를 사용한다.
  • Head 컴포넌트 안에 <title> 태그를 추가하고 원하는 타이틀을 작성할 수 있다. 또한, <meta> 태그를 추가하여 페이지의 메타 정보를 설정하거나, <link> 태그를 추가하여 favicon과 같은 리소스를 페이지에 추가할 수도 있다.
// components / Seo.js
import Head from 'next/head';

export default function Seo({ title }) {
  return (
    <Head>
      <title>{`${title} | Seul's Movies`}</title>
      <meta content="This is Seul's Movies website" />
    // <link rel='icon' href='/favicon.ico' />
    </Head>
  );
}

link 태그를 넣지 않아도 잘 적용되는 것을 확인, 이유가 뭘까? 🤔

✍️ public 폴더 내부에 favicon.ico 파일을 위치시키면, Next.js는 자동으로 해당 파일을 head 요소에 link 태그로 추가해준다. 이는 Next.js가 내부적으로 next/head 모듈의 Head 컴포넌트에서 자동으로 처리하는 것인데, 그래서 별도로 Head 컴포넌트를 사용하여 link 태그를 추가해주지 않아도 자동으로 favicon이 적용된다고 한다. 😀




이미지 가져오기

📌 Image - next.js 공식

1. public 폴더 안의 svg 파일을 가져오기

ex.) public / vercel.svg 파일 가져오기
아래처럼 작성했더니 eslint 에 노란줄이 떴다.😮

// _app.js
<img src='/vercel.svg' alt='vercel_logo' />

2. 👉 <Image> 활용하기

import Image from 'next/image';

...
 <Image src='/vercel.svg' alt='vercel_logo' width={200} height={200} />

3. Image 스타일링

<div className='container'>
        <Seo title='Home' />
        {movies?.map((movie) => (
          <div className='movie' key={movie.id}>
			{/* <img src={`https://image.tmdb.org/t/p/w300${movie.poster_path}`} /> */}
            <div className='image-wrapper'>
              <Image
                src={`https://image.tmdb.org/t/p/w300${movie.poster_path}`}
                width={300}
                height={400}
                alt='movie_img'
              ></Image>
            </div>

            <h4>{movie.original_title}</h4>
          </div>
        ))}
      </div>

<style jsx>{`
	...
        .container {
          display: grid;
          grid-template-columns: 1fr 1fr;
          padding: 25px;
          gap: 30px;
        }

        .image-wrapper {
          margin: auto;
          max-width: 300px; // max-width:100% 하면 가운데 정렬이 되지만 border-radius가 잘 적용 안됨
          max-height: 400px;
          margin-bottom: 10px;
          border-radius: 12px; // 1)
          overflow: hidden; // 2) 이렇게 1)과 같이 설정해 줌으로써 속성을 적용시킬 수 있다.
          transition: transform 0.2s ease-in-out;
        }

        .image-wrapper:hover {
          transform: scale(1.02) translateY(-5px);
        }
	...
      `}</style>
  • img 태그 대신 next.js에서 제공하는 Image 컴포넌트를 활용한 스타일링이다.
  • 이렇게 Image 컴포넌트에 스타일을 적용하기 위해서는 먼저 꼭 필요한 프로퍼티를 지정해주고 wrapper class를 만들어 준 뒤 스타일을 적용해줄 수 있다.
  • 아래의 next.config.js도 별도로 설정해줘야 했다. 👇

next.config.js 에서 설정하기

const nextConfig = {
  reactStrictMode: true,
  async redirects() {
   ...
  },
  async rewrites() {
    ...
  },
  // 다른 설정 추가 가능
  images: { // 1)
    domains: ['image.tmdb.org'],
  },
};

module.exports = nextConfig; // 위에서 정의한 nextConfig 객체 내보내기
  • 1) next.config 설정에서 images 도메인을 추가해준다. 해당 도메인은 에러창에서 확인하여 지정해 줄 수 있었다. 😎




Font 적용하기

@next/font 이용하기
📌 Optimizing Fonts - next.js 공식

  • next/font 는 개인 정보 보호 및 성능 향상을 위해 자동으로 글꼴을 최적화한다.
  • 브라우저에서 Google로 요청을 보내지 않는다.
  1. @next/font 패키지를 설치한다. (추후 안정화가 진행되면 next.js 팀에서 안정 패키지를 추가해서 별도 설치를 하지 않아도 된다고 한다.😮)
  2. Google 글꼴에서 글꼴 선택
  3. 글꼴을 가져와서 사용하기
// app.js
import { Poppins } from '@next/font/google';

const title = Poppins({
  weight: ['400', '600'],
  subsets: ['latin'],
});

const text = Ubuntu({
  weight: ['400', '700'],
  subsets: ['latin'],
});

export default function App({ Component, pageProps }) {
  return (
    <>
      <h1 className={title.className}>title </h1>
      <p className={text.className}>description</p>
    </>
  );
}

❗️ 폰트가 적용되지 않았던 문제 해결하기
✍️ props 전달 태그 위치와 global.css의 지정값 때문이었다.

// globals.css
* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  /* font-family: 'Poppins', sans-serif; */
}


// app.js
export default function App({ Component, pageProps }) {
  return (
    <div className={poppins.className}>
      <Layout> // 이곳에 className을 넣었더니 적용되지 않았다. 
        <Component {...pageProps} />
        <span>🌈 안녕 🌈</span>
      </Layout>
    </div>
  );
}

_document.js 파일 활용하기

_document.js 파일은 Next.js에서 HTML 문서를 커스터마이징하기 위해 사용되는 특수한 파일로, 이 파일은 pages 폴더 안에 위치시켜야 한다. 🤔

_document.js 파일에 Google Fonts 코드를 추가하면 전체 애플리케이션에서 Google Fonts를 사용할 수 있다.

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head>
          <link rel='preconnect' href='https://fonts.googleapis.com' />
          <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin />
          <link
            href='https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;500&display=swap'
            rel='stylesheet'
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;




Data Fetching

영화정보 제공 사이트에서 API 키를 발급받고 데이터 받아오기
👉 The Movie Database API

1) 리액트 훅 useEffectfetch()

  const API_KEY = 'd43... (내 API_KEY)';

  useEffect(() => {
    fetch(`https://api.themoviedb.org/3/movie/popularapi_key=${API_KEY}`)
      .then((response) => response.json())
      .then((data) => console.log(data.results));
  }, []);


2) 데이터가 비어있을 때 에러 대비하기

  // const [movies, setMovies] = useState([]);
     const [movies, setMovies] = useState();

  return (
	...
    // {movies.map((movie) => (
       {movies?.map((movie) => (
        <div key={movie.id}>{movie.original_title}</div>
    </div>
  • useState([]) 의 형태를 변경하면서 return도 같이 수정해준다.

2) async await 로 변경하기
추가로 참고한 문서

  useEffect(() => {
    async function fetchMovies() {
      const response = await fetch(
        `https://api.themoviedb.org/3/movie/popular?api_key=${API_KEY}`
      );
      const data = await response.json();
      return data.results; // return값을 줘야
    }
    fetchMovies().then((movieData) => setMovies(movieData)); // 여기서 받아옴
  }, []);

+) 에러 핸들링 (tyr ...catch)

  useEffect(() => {
    async function fetchMovies() {
      try {
        const response = await fetch(
          `https://api.themoviedb.org/3/movie/popularf?api_key=${API_KEY}`
        );

        if (!response.ok) {
          const message = ` 에러 상태 👉 : ${response.status}`;
          throw new Error(message);
        }

        const data = await response.json();
        return data.results;
      } catch (error) {
        console.error(error);
      }
    }
    fetchMovies().then((movieData) => setMovies(movieData));
  }, []);


fetch 메서드를 사용, REST API를 호출하고 HTTP 응답 코드가 성공적인지 확인한다.
→ 응답이 실패하면 throw new Error() 문을 사용, 에러를 던진다.
→ 그리고 try 블록 안에서 response.json() 을 사용해서 응답 데이터를 JSON 형식으로 변환한다.
→ 만약 JSON 파싱이 실패하면 catch 블록으로 제어가 이동한다.

(이것보다 더 나은 방법이 있는지, 다른 식으로 작성할 수 있는지 열심히 찾는중.. 🧐)




API key 숨기기

📌 next.config.js - next.js 공식

  • API key를 감추지 않고 자바스크립트 코드에 그대로 사용한다면 우리의 API key가 노출된다. (개발자도구 or 소스코드 ...)
    그러므로 API key를 숨기는 방법을 알아보도록 하자. 🔥

✍️ Redirects & Rewrites
Redirects와 Rewrites 는 Next.js의 라우팅 기능 중 하나이다.


1) Redirects (URL변경됨)
: 클라이언트가 요청한 URL을 다른 URL로 자동으로 리디렉션하는 방법
→ 이를 통해서 사용자 경험을 향상 시키는 데 도움을 줄 수 있다. 예를 들어, 사용자가 이전 URL을 북마크 해두었다면 URL이 새 URL로 리디렉션되어 페이지를 올바르게 표시할 수 있기 때문 😎

async redirects() {
    return [
      {
        source: '/movies',
        destination: 'https://google.com',
        permanent: false,
      },
    ];
  },
  • Redirect을 사용하면 들어오는 request 경로를 다른 destination 경로로 Redirect할 수 있다.
  • Redirect을 사용하려면 next.config.js에서 redirects 키를 사용할 수있다.

2) Rewrites (URL변경되지 않음)
: 클라이언트가 요청한 URL을 다른 URL로 대체하는 방법
→ 이를 통해 클라이언트는 실제로는 다른 URL에 접근하고 있지만, URL이 변경되지 않은 것처럼 보이게 된다.
(이를 URL 프록시 역할이라고 부르는데, 이는 프록시 서버와 유사한 개념이다. 클라이언트가 요청한 URL을 중계하여 다른 URL로 변경해주는 것이 프록시 서버의 역할과 유사하기 때문이다.)
따라서 URL의 보안성을 강화하거나, 더 나은 사용자 경험을 제공할 수 있게된다.✨

async rewrites() {
    return [
      {
        source: '/blog/:path*',
        destination: 'https://blog/news/:path*',
        permanent: true,
      },
    ]
  },
  • Rewrites를 사용하면 들어오는 request 경로를 다른 destination 경로에 매핑할 수 있다.
  • Rewrites은 URL 프록시 역할을 하고 destination 경로를 mask(원래의 내용을 가리거나 숨김)하여 사용자가 사이트에서 위치를 변경하지 않은 것처럼 보이게 한다.
    반대로 redirects은 새 페이지로 reroute되고 URL 변경 사항을 표시한다.

👉 이 특징을 이용해서 API key를 숨길 수 있다.

// next.config.js
const API_KEY = process.env.API_KEY;

async rewrites() {
    return [
      {
        source: '/api/movies',
        destination: `https://api.themoviedb.org/3/movie/popular?api_key=${API_KEY}`,
      },
    ];
  },

+) getServerSideProps 를 사용해서 숨길 수도 있다. 😮




getServerSideProps

📌 getServerSideProps - next.js 공식
📌 TypeScript와 같이 사용하기

  • page에서 서버 측 랜더링 함수인 getServerSideProps함수를 export하는 경우 Next.js는 getServerSideProps에서 반환된 데이터를 사용하여 각 request에서 이 페이지를 pre-render한다.
  • getServerSideProps는 서버 측에서만 실행되며 브라우저에서는 실행되지 않는다.
  • ✍️ pre-render(브라우저에서 페이지를 로드하기 전에 서버 측에서 페이지를 생성하는 기술)
// ex.)
  export default function Home({ data }) {
  // 데이터 랜더링
  }

  // 매 request마다 실행
  export async function getServerSideProps() {
  const res = await fetch(`https://.../data`);
  const data = await res.json();

  // props를 통해 page에 data전달
  return { props: { data } }
  }

+) ✍️ 기존 데이터 패치를 이곳에서 작성해보기

export async function getServerSideProps() { 
  // 1)
  const response = await fetch('http://localhost:3000/api/movies'); // 2)
  const data = await response.json();
  return {
    props: {
      data: data.results,
    },
  };
}
  • 1) 이 안의 코드는 server에서 돌아가게 된다.(백엔드에서만 실행)

    • 따라서 API key를 여기에 써줘도 클라이언트에 보이지 않는다.
    • 원한다면 async를 사용할 수도 있다.
    • 이 함수는 object를 리턴한다. object 안에는 props라는 key가 들어있다. key안에는 아무 데이터를 넣을 수 있다.
    • 그 데이터를 위의 Home 컴포넌트의 매개변수로 가져와줄 수 있다. -> 즉 return하여 props로써 page에게 넘겨준다.
  • 2) 클라이언트에서 API를 받아오는 것이 아니므로 http...을 다 기재해줘야 정상적으로 작동한다.

  • 여기서 에러 핸들링 처리는 어떤식으로 해야할까? 🧐


📌 언제 getServerSideProps를 사용해야 할까?

  • request time에 반드시 데이터를 fetch해와야 하는 페이지를 pre-render해야 하는 경우에만 getServerSideProps를 사용해야 한다.
    데이터를 pre-render할 필요가 없다면 클라이언트 측 또는 getStaticProps에서 데이터를 가져오는 것을 고려해야 한다고 한다. 🧐

📌 클라이언트 측에서 데이터 가져오기

  • 페이지에 자주 업데이트되는 데이터가 포함되어 있고 데이터를 미리 렌더링할 필요가 없는 경우 클라이언트 측에서 데이터를 가져올 수 있다 . 예를 들면 사용자별 데이터 등이 있다.
  1. 먼저 데이터가 없는 페이지를 즉시 표시합니다.
  2. 페이지의 일부는 Static Generation을 사용해 pre-render할 수 있습니다.
  3. 없는 데이터를 위해 loading 상태를 표시할 수 있습니다.
  4. 그런 다음 클라이언트 측에서 데이터를 가져와 준비가 되면 표시합니다.
  • 예를 들어 이 접근 방식은 사용자 대시보드 페이지에 적합하다. 대시보드는 비공개의 사용자별 페이지이므로 SEO와 관련이 없으며 페이지를 미리 렌더링할 필요가 없고, 데이터는 자주 업데이트되므로 요청 시 데이터를 가져와야 하기 때문이다.




라우팅하기

✍️ 영화 포스터나 제목을 클릭했을 때 /movies/[id] 로 라우팅 해보기

ex.) http://localhost:3000/movies/315162 주소의 파일 위치 : pages/movies/[id].js

1. Link 태그를 통해서 라우팅하기

<Link href={`/movies/${movie.id}`} legacyBehavior>
  <a>{movie.original_title}</a>
</Link>

1-1. Link 태그와 as 속성

 <Link
  href={{
    pathname: `/movies/${movie.id}`,
    query: {
      title: movie.original_title,
    },
  }}
  as={`/movies/${movie.id}`}
  legacyBehavior
>
  <a>{movie.original_title}</a>
</Link>
  • href: 클릭 시 이동할 페이지 경로를 설정. 여기서는 /movies/${movie.id} 로 설정했다.
  • query: 페이지로 전달할 쿼리 파라미터를 설정. 여기서는 title이라는 쿼리 파라미터에 movie.original_title 값을 전달하도록 설정했다.
  • as: 브라우저 주소창에 표시될 경로를 설정. 이는 href에 설정한 경로에 대응되는 서버 측 경로로 연결된다. 여기서는 /movies/${movie.id}를 설정했다.
  • legacyBehavior: 이전 버전의 Next.js에서 사용하던 URL을 사용할 때 설정하는 옵션.

👉 즉 /movies/${movie.id}로 링크를 설정하되, title 쿼리 파라미터를 movie.original_title 값으로 전달하고, 브라우저 주소창에는 /movies/${movie.id}가 표시된다.


2. useRouter() 훅을 사용해서 라우팅하기

  • 컴포넌트 내부에서 router를 사용하면 router는 프론트에서만 실행된다.
import { useRouter } from 'next/router';

const router = useRouter();
const onClick = (id) => {
  console.log(id);
  router.push(`/movies/${id}`);
};

2-1. router.push 메서드

const onClick = (id, title) => {
  router.push(
    {
      // URL 설정, 정보
      pathname: `/movies/${id}`,
      query: {
        title,
      },
    },
    // as : 브라우저에 보이는 URL을 마스킹
    `/movies/${id}`
  );
};
  • router.push 메서드는 클라이언트 측에서 동적 라우팅을 위해 사용한다.
  • router.push 메서드는 첫 번째 인자로 URL 설정 객체를 받으며, 이 객체에는 pathnamequery 프로퍼티가 있다.
    pathname 은 요청할 페이지의 경로를 나타내며, query 는 해당 페이지에 전달할 쿼리 매개변수를 나타낸다.
  • 두 번째 인자로는 브라우저에 보이는 URL을 설정할 수 있는데, 이것을 as 라고 부른다.
    as 를 사용하여 브라우저의 URL을 마스킹할 수 있다. 즉, pathname 은 서버에서 실제 페이지 경로를 나타내고, as 는 클라이언트에서 보여지는 URL을 나타낸다.

3. getServerSideProps 이용하기

export default function Detail({ params }) {
  const [title, id, src] = params || []; // 1)
  return (
    <div>
      {/* 검색엔진최적화(SEO)에 도움이 될 수 있다. */}
      <Seo title={title}></Seo>
      <h4>{title}</h4>
    </div>
  );
}

// fetch를 위한 것이 아니라 조금 더 빠르게 데이터를 가져오기 위해서 쓸 경우
export function getServerSideProps({ params: { params } }) { // 2)
  // console.log(params);
  return {
    props: { params },
  };
}
  • 1) [] 를 넣지않으면 undefined 때문에 에러가 뜨는 듯하다. 🤔
    +)
    아직 js들이 다운로드가 안된상태이므로 useRouter()로 정보를 제대로 못가져오고 있다.
    따라서 초기에는 빈 배열을 추가해줘서 오류가 발생하지 않도록 해주고, js가 내려가서 다시 렌더링하게되면 그 때는 빈 배열이 아닌 router.query.params 에서 값을 가져올 수 있게 된다고 한다. 😮

  • 2) next.js가 server-side context를 제공해준다.

profile
기억보단 기록을 ✨

0개의 댓글