Next.js 시작하기(DEPLOYMENT )

짜스의 하루 ·2024년 7월 23일

CSS Modules

우선, 모든 페이지에 적용할 global.css를 생성해보자

styles 파일을 생성하고, 안에 global.css 폴더를 하나 생성한 후, 모든 페이지에 기본적으로 적용할 css 속성들을 지정해 두었다.

그럼 이 global.css를 어디서 import를 해야할 까?
--> 전역 layout.tsx에 적용하면 된다!

  • css 모듈이 아니고, 우리의 layout에 import한 일반 css파일이다.
  • 기본적으로 모든 페이지들은 이 layout으로 구성되기 때문에, 그것들은 모두 같은 css를 공유하게된다.

global.css는 모든 파일에 적용되게 되는데, 그럼 navigation에만 적용하고 싶을 때에는 어떻게 해야할까?

--> CSS Modules

Next.js에는 .module.css 확장자를 사용하여 CSS 모듈을 기본적으로 지원하게 된다.
CSS 모듈은 고유한 클래스 이름을 자동으로 생성하여 CSS 범위를 로컬로 지정한다.
이를 통해 충돌에 대한 걱정 없이 다른 파일에서 동일한 클래스 이름을 사용할 수 있다.
이러한 동작으로 인해 CSS 모듈은 컴포넌트 레벨 CSS를 포함하는 이상적인 방법이 되었다.

보통 css modules를 이용해서 css속성을 사용하면, 해당 파일 옆에다 만들거나, styles 파일 안에 따로 보관하기도 한다

CSS Modules의 특징

  • classname만을 생성
  • javascript 파일인 것처럼 import
  • 이렇게 하면 class의 충돌은 절대 없을 것임
  • 일반 적인 것은 global파일에 넣어야 함
    --> ul 이런식은 ul모든 파일에 적용됨

자, 이제 css modules를 직접 만들어보자

navigation.module.css
여기서 중요하게 생각해야 하는 점은, .module.css를 꼭 사용해야 한다는 점이다.
앞에 navigation은 nav, navigationCSS등 아무거나 사용해도 상관없다.

그리고 className으로 생성해야 한다는 점이다
만약, .nav가 아닌 nav로 작성하게 되면 해당 모든 파일의 nav 속성에 적용되기 때문에, className을 지정해야 한다.

이렇게 지정한 css를 어떻게 불러와서 사용할까?

import styles from '파일명' 으로 styles를 불러온 뒤, css파일에서 지정한 className을
<nav className={styles.nav}>형식으로 가져다 써야 한다

여기서, .nav ul은 왜 <ul className={styles.nav ul}>로 안해요?
--> 여기서 .nav ul은 className이 nav인 속성 안에 있는 ul이라는 의미로 따로 기입하지 않아도 알아서 찾아서 ul에 css 속성을 적용시켜준다!


여기서 확인할 수 있듯이, CSS 모듈은 클래스 이름을 고유하게 만들어서 전역 네임스페이스 충돌을 방지한다.

예를 들어, nav라는 클래스를 CSS 파일에 정의하면, 이 클래스 이름은 컴파일 과정에서 고유한 이름으로 변환된다.

styles.nav가 navigation_nav___kX_6와 같은 형태로 변환된 것을 확인할 수 있다!


home.module.css

이제 메인 home 화면을 꾸며보자.

그 이전에 우선 메인 (home)/page.tsx 에서 컴포넌트 분리를 먼저 시켜보자.

(home)/page.tsx

import { Metadata } from 'next';
import Movie from '../../components/movie';
import styles from '../../styles/home.module.css';

export const metadata: Metadata = {
  title: 'Home ',
};

export const API_URL = 'https://nomad-movies.nomadcoders.workers.dev/movies';

async function getMovies() {
  // await new Promise((resolve) => setTimeout(resolve, 5000));
  return fetch(API_URL).then((response) => response.json());
}

export default async function HomePage() {
  const movies = await getMovies();

  return (
    <>
      <div>
        {movies.map((movie) => (
          <Movie
            key={movie.id}
            id={movie.id}
            poster_path={movie.poster_path}
            title={movie.title}
          />
        ))}
      </div>
    </>
  );
}
  • fetch로 받아온 movies값을 <Movie/> 컴포넌트로 id, poster_path, title를 넘겨주면서, <Movie/> 컴포넌트를 따로 분리해 주었다.

components/movie.tsx

'use client';

import Link from 'next/link';
import styles from '../styles/movie.module.css';
import { useRouter } from 'next/navigation';

interface IMovieProps {
  title: string;
  id: string;
  poster_path: string;
}

export default function Movie({ id, title, poster_path }: IMovieProps) {
  const router = useRouter();
  const onClick = () => {
    router.push(`/movies/${id}`);
  };
  return (
    <>
      <div>
        <img src={poster_path} alt={title} onClick={onClick} />
        <Link href={`/movies/${id}`}>{title}</Link>
      </div>
    </>
  );
}
  • page.tsx에서 받은 id, title, poster_path의 타입들을 IMovieProps로 정해준 뒤,
    <img/> 로 img를 받아오고, title를 <Link/>로 , title를 눌렀을 때, 해당 영화의 id로 이동할 수 있도록 (상세 정보 보기) 코드를 작성해 두었다.

  • 여기서, img를 눌렀을 때에도 /movies/${id} 로 이동할 수 있도록 처리해 주고 싶은데 어떻게 처리해야 할 까?

--> 이때 사용되는게 useRouter()메서드이다.

useRouter 사용:

  • useRouter 훅을 사용하여 router 객체를 가져온다.
  • onClick 함수는 router.push(/movies/${id}) 를 호출하여 클릭 시 해당 경로로 이동한다.

이미지 클릭 이벤트:

  • <img src={poster_path} alt={title} onClick={onClick} className={styles.poster} /> 코드에서 onClick 속성을 통해 이미지 클릭 시 onClick 함수가 호출되어 해당 영화의 상세 페이지로 이동하도록 코드를 작성해 주었다.

그럼 이제, movie.tsx 부터 movie.module.css 로 css를 적용시켜보자

movie.module.css

  • import로 해당 movie.module.css를 styles로 불러온 뒤,
    css를 적용할 태그에 className을 styles.클래스명 이렇게 주면 된다!

home.module.css
이렇게 적용하면!

요런 화면이 완성되는 것을 확인할 수 있다!


MovieInfo & MovieVideos

이제 해당 영화를 하나 클릭했을 때,
http://localhost:3000/movies/519182 해당 주소로 들어가게 되는데, 이때 보이는 화면을 구성해보려고 한다!

/movies/id의 구조는 movies/[id]인것은 이제 꼭 알고 있어야 하는 부분이다!

page는 <Suspense/>로 감싼 <MovieInfo/>, <MovieVideo/> 두개가 있다
<Suspense/>로 감싼 이유는 ?
--> 데이터를 불러올 때, 데이터를 불러오는 대로 화면을 꾸릴 수 있고, 이외의 UI는 사용자에게 제공할 수 있다.

movie-info.tsx

import { API_URL } from '../app/(home)/page';

async function getMovies(id: string) {
  return await fetch(`${API_URL}/${id}`).then((response) => response.json());
}

export default async function MovieInfo({ id }: { id: string }) {
  const movie = await getMovies(id);

  return (
    <>
      <div>
        <img src={movie.poster_path} alt={movie.title} />

        <div>
          <div>
            <h1>{movie.title}</h1>
            <a href={movie.homepage} target={'_blank'}>
              HomePage
            </a>
          </div>
          <h3>⭐️ {movie.vote_average.toFixed(1)}</h3>
          <p>{movie.overview}</p>
        </div>
      </div>
    </>
  );
}
  • getMovies()로 받아온 데이터 movie에서 poster_path , title, homepage , vote_average , overview 를 받아와서 화면에 뿌린다
  • <a/> 태그를 이용해서 HomePage를 만든 이유는 -> target={'_blank'}를 통해 새로운 페이지에서 homepage가 열릴 수 있도록 하기 위해서이다.

이제, movie-info.module.css를 만들어보자

movie-info.module.css

이후 화면을 출력해보면 !


movie-video.tsx

import { API_URL } from '../app/(home)/page';

async function getVideos(id: string) {
  return await fetch(`${API_URL}/${id}/videos`).then((response) =>
    response.json()
  );
}

export default async function movieVideo({ id }: { id: string }) {
  const videos = await getVideos(id);

  return (
    <>
      <div>
        {videos.map((video) => (
          <iframe
            key={video.id}
            src={`https://youtube.com/embed/${video.key}`}
            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
            allowFullScreen
            title={video.name}
          />
        ))}
      </div>
    </>
  );
}

movieVideo 컴포넌트:

  • 비동기 함수로 구현되어 있으며, 특정 영화 ID를 받아 비디오 데이터를 가져온다.
  • getVideos 함수를 호출하여 비디오 데이터를 가져온 후, videos 배열에 저장한다.
  • videos 배열을 map 메서드를 사용하여 순회하면서 각 비디오를 <iframe> 태그로 렌더링한다.

<iframe> (inline frame)은 HTML에서 다른 HTML 페이지를 현재 문서에 삽입할 때 사용하는 태그이다. 이는 웹 페이지 안에 다른 웹 페이지를 포함할 수 있게 한다

  • <iframe> 태그에는 key 속성이 있어 React에서 리스트 렌더링 시 각 요소를 구분할 수 있게 한다.
  • src 속성은 YouTube의 임베드 URL로 설정되어 있어, 비디오를 임베드한다.
  • allow 속성은 다양한 기능(가속도계, 자동 재생 등) 을 허용한다.
  • allowFullScreen 속성은 비디오가 전체 화면으로 재생될 수 있도록 한다.
  • title 속성은 비디오의 이름을 설정하여 접근성을 높인다.

그럼 이제 movie-video.module.css를 생성해보자

movie-video.module.css

이제 화면을 출력해보면!

짠 ! 예고편, 티저 등 관련 영상들이 쫙 출력되는 것을 확인할 수 있다


generateMetadata

정적인 HTML 페이지에서 메타데이터를 이용해 <head>에 들어갈 내용을 정의하고 페이지의 제목(title)을 설정할 수 있었다.

export const metadata: Metadata = {
  title: 'Home ',
};

(home)/page.tsx의 경우, 정적인 페이지이기 때문에, metadata에 title를 지정할 수 있었다.

그럼, 이처럼 /movies/영화id동적으로 바뀌는 페이지에서는 어떻게 해야할까?

이때 사용할 수 있는 것이 generateMetadata()함수를 만드는 것이다

  • 인수 (IParams):
    generateMetadata 함수IParams 타입의 객체를 인수로 받는다.
    --> 이 객체는 params 속성을 가지며, 이 속성은 id를 포함한다.

  • 영화 데이터 가져오기:
    getMovie(id) 함수를 호출하여 주어진 id에 해당하는 영화 데이터를 가져온다.
    getMovie 함수는 id를 인수로 받아 해당 영화의 데이터를 반환한다.

  • 메타데이터 반환:

  • title 속성을 가진 객체를 반환 --> movie.title을 제목으로 설정
    이 반환값은 Next.js가 <head> 태그 안에 <title> 태그로 삽입

  • 메타데이터 생성의 목적:
    SEO (Search Engine Optimization): 검색 엔진이 페이지를 인덱싱할 때 도움이 된다.
    사용자 경험 향상: 브라우저 탭이나 검색 결과에서 페이지 제목을 통해 내용을 미리 볼 수 있다.

이렇게 선택한 영화의 title을 받아와 head 태그의 title에 동적으로 변경되는 것을 확인할 수 있다!


배포

우리는 배포를 하기 위해서 우선 깃허브에 모든 변경사항을 push해줄 것이다!

vercel 에서 내 깃허브 주소와 연결 (?)을 해주고, Add New... 버튼에서 Project를 누르면, 이렇게 내 깃허브 레포지토리들이 뜨는데 해당 레포지토리를 import 를 눌러주면 된다.

그 후 페이지에서 Deploy 버튼을 클릭하여 프로젝트를 배포하면 된다!
Vercel은 자동으로 레포지토리의 최신 코드를 가져와 배포하게 된다
또한, package.json 폴더에서 build 와, start를 추가해주면 된다

https://nextjs-movies-five-rose.vercel.app/ 주소에서 build가 된 것을 확인할 수 있다!

profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글