기억의 실마리
2024. 10. 20. 18:46

🤔NextJS 쿠키?

브라우저에 저장되는 일반적인 쿠키와 다를게 없지만

NextJS는 SSR을 지원한다는 점에서

cookie를 서버에서 확인할 수 있는 방법이

제한적이기 때문에 CSR(React 등)만 활용하여

개발하던 개발자들은 생각보다 더 난해한 문제를

겪게 될 수 있다.

 

그런 개발자 중 한명이 필자였고 특히

쿠키를 활용한 JWT 로그인 유지를 구현할 때

interceptor를 통해 매번 토큰을 header에 넣어 요청하고

accessToken이 만료된 경우에는 refreshToken을

서버에 재발급요청하여 재생성된 토큰들을 쿠키에 저장하고

저장된 쿠키를 통해서 이전 요청을 재요청하는 프로세스에서

accessToken 쿠키가 undefined인 상태로 요청이 되는 이슈

꽤나 험난한 삽질과 많은 시간을 고민하게 되었다.

 

결과적으로 NextJS를 제대로 다루기 위해서는

hydration,

middleware,

server - component,

server - actions / mutations,

위 4가지의 작동방식은 필수적으로 학습을 통해

Next가 작동되는 방식에 대해 이해하고 있어야한다는

생각이 들었다.

 

🫗hydration

(hydration에 대한 글은 개인적 주관이 강한 글이기 때문에 넘어가셔도 좋습니다😊)

필자는 단순히 하이드레이션을 주입식 교육처럼

하이드레이션 = 서버의 결과를 클라이언트로 합치는 것

이라고만 생각했지만 결과적으로 이해하게 된 계기는

여러 이슈를 겪으며 어원에 대해 다시 생각하는 것으로

이해도를 갖추게 된 것 같다.

 

hydration이란 수분공급 이라는 어원을 가지고 있다.

그렇다면 어째서 수분공급이지? 라는 의구심을 가지고

좀 더 단순하게 어원에 대해 생각했을 때 식물이 뿌리에서

흡수한 물을 줄기를 통해서 수분공급을 하는 방식을 떠올려 봤다.

 

뿌리 = Server

(땅속에 있어 우리가 표면적으론 볼 수 없지만 핵심이 되는 뿌리)

 

줄기, 잎, 꽃 등 = Client

(땅을 파지 않아도 겉으로 볼 수 있는 식물의 외형)

 

식물은 수분공급이 제대로 이루어지지 않는다면

그 식물은 시들어버리거나 제대로 성장하지 못한다.

 

그렇기 때문에 Server에서 보여주고자 하는 것을

Client에 공급해주어 제대로된 결과물을 보여주는 것이

Hydration이구나! 라는 결론을 내리게 되었다.

 

물론 그저 나의 상상력으로 여기까지 온 것일 수도 있다...

하지만 좀 더 어원에 대해 이해하려할 수록

이해도가 높아지는 경우가 생각보다 많았던 것 같다.

 

만약 영어가 아닌 한글이었다면 조금 더 직관적으로

프로세스를 이해했을 수도 있지만 프로그래밍 언어가

기본적으로 영어로 되어있기 때문에 우리가 프로그래밍에

대해서 이해하기 어렵게하는 하나의 벽이 되고있지 않나...

하는 생각이 들곤 한다.

 

🔗middleware

middleware는 server에서 client에 렌더링되기 이전에

거치게 되는 곳으로 서버와 클라이언트의 교집합 지점이라고

볼 수도 있는 영역이다. 때문에 NextJS를 다룬다면 정말 중요한

기능이기 때문에 꼭 알아야된다고 생각하는 것 중 하나다.

 

NextJS를 활용해서 사용자에게 보여주기전에 server에서

내부적으로 redirect하거나 필수적인 쿠키를 미리 설정

가능하고 국제화를 하는 경우에는 페이지에 접속한

국가에 적합한 언어로 구성된 페이지를 제공하기 위해서

활용하기도 한다.

 

middleware 활용처는 너무 많기 때문에 예제코드는

필자의 깃허브 링크로 남겨두겠습니다.

 

* nextjs14 app-router 국제화를 적용한 템플릿을 예제로 넣었습니다.

Github: https://github.com/zeriong/next14-app-router-i18n-template/blob/master/middleware.ts

 

⚙️server-component

NextJS에서 컴포넌트를 구성할 때 최상단에 "use client"를

작성하지 않는다면 기본적으로 server-component로 작성된다.

그리고 "use client"를 작성한다면 react에서 사용되는 CSR관련

Hooks를 활용할 수 있게 된다.

 

단 여기서 중요한 것은 "use client"라고 해서 SEO가 안되는 것은

아니라는 점이다.

 

html 구성은 server에서 렌더링이 되기 때문에 SEO는

영향을 끼치지 않는다. 단지 CSR에서 활용 가능한

React의 Hooks를 활용해서 CSR에서 작동되는 JS를 활용하겠다.

라고 선언하고 활용하는 것에 불과하다.

 

그러니 "use clinet"를 썼다고 해서 SEO가 안될 것이라는

걱정은 하지 않아도 될 것이다.

(useEffect를 활용하여 state 변경후에 생성되는 컴포넌트 제외)

 

 

🪄server - actions / mutations

서버에서 실행되는 비동기함수를 뜻한다.

form에서 활용하는 action이나 기타 fetch를 활용한

api요청 등을 server에서 작동하게 만드는 것이다.

 

활용방식은 아주 간단하게 해당 함수의 환경 최상단에

"use server" 를 선언하게 되면 이는 server에서 작동하는

함수로 정의된다.

 

"use server"를 정의하는 환경은 주로 파일단위나

함수단위로도 활용 가능하다.

 

상황에 따라서 우리는 다양하게 활용하지만 주로

하나의 파일에서 "use server"를 최상단에

정의하여 모듈화하여 활용하는 경우가 보편적이다.

 

예시적으로 actions.ts 파일을 생성한다고

가정했을 때의 예제 코드이다.

 

  • server-actions 예제코드
// actions.ts

"use server";

/**@desc login server-action */
export async function loginAction(prevState: { success?: boolean; msg?: string }, formData: FormData) {
  const userId = String(formData.get("userId")).trim() as string;
  const userPassword = String(formData.get("userPassword")).trim() as string;

  if (!userId.length || !userPassword.length) return { success: false, msg: "아이디 또는 비밀번호를 확인해주세요" };

  const reqBody = { userId, userPassword };

  const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL_AUTH}/auth/login`, {
    method: "POST",
    body: JSON.stringify(reqBody),
  });

  const resBody = await res.json();

  if (resBody.code != 200) return { success: false, msg: resBody?.message };

  return { success: true, msg: "success" };
}

 

위처럼 server-action을 구성하고 해당 server-action을 로그인을 하는 컴포넌트에서 useFormState 훅을 활용해서 state로 response 결과값을 client에서 동적으로 확인할 수 있다.

 

  • server-mutations 예제코드
// mutations.ts

"use server";

import { getUserInfo } from "@/apis/user";

/** get user data */
export async function getUserData() {
  const res = await getUserInfo();
  return res.body.data;
}

 

정말 단순하게 "use server"를 명시해주어 server에서 해당 api로 fetch를 통해 response 값을 받아볼 수 있다.

 

server - actions / mutations 장점

  1. network 요청 수를 줄일 수 있다
  2. server 내부에서 미리 값을 fetch 받아 렌더링하기 때문에 SEO 향상
  3. API route 없이 서버 측에서 직접 DB에 접근가능
  4. 민감한 데이터를 클라이언트 측에 노출시키지 않을 수 있음
  5. client side 번들 사이즈가 줄어 초기 로딩 속도 개선

 

🍪NextJS 쿠키 트러블 슈팅

내가 맞닥뜨린 이슈는 결국 NextJS에서 쿠키에 대한

hydration이 필요한 상황이었고 가장 첫번 째로 고민한 것은

가능한 것에 대한 고찰이었다.

 

서버에서 쿠키에 접근가능한 방식을 생각해봤을 때

next header에 접근할 수 있어야하는 것이

우선 선행이 되어야 했다.

 

그렇다면 어떠한 경우에 server에서 쿠키에 접근할 수 있는가?

내가 떠올릴 수 있는건 두가지였다.

 

1. middleware의 NextRequest를 통한 접근

2. server-component에서 next-header의 cookies()로 접근

 

1번의 경우는 현실성이 부족했고 2번으로 접근하기로 했다.

 

하지만 interceptor를 구성할 때 따로 api를 요청할 때 마다

지속적으로 재사용해야 하기 때문에 파일로 분기하여

구성할 수 밖에 없었다.

 

단순히 fetch에 interceptor를 구성하여 붙이는 코드로 구성되어있어

최상단에 "use server"를 명시하는 것도 말이 안되고,

그렇다고 interceptor를 server-component에서 구성하는 것도

말이 안된다고 생각이 들었다.

 

그때 마침 생각난 것이 바로 server-component가

next-header에 접근할 수 있는 이유에 대해서 생각해봤다.

 

nextjs14에서는 컴포넌트 구성 시 최상단에

"use client"를 명시하지 않으면 서버컴포넌트로 정의된다.

 

이는 즉 이전 버전에서 사용하던 "use server"가 생략됐을 뿐이고

server - actions / mutations에서 최상단에 "use server"를

명시하고 활용하는 것과 동일하다고 생각이 들었다.

 

그렇다면 방법은 하나 뿐이다.

utils에 server 폴더를 만들고 최상단에 "use server"를 명시한

cookies.ts파일을 만들어 내부에 유틸을 구성하는 방법이다.

 

getServerSideCookie,

setServerSideCookie,

removeServerSideCookie,

 

위 세가지 쿠키관련 유틸을 구성하고

next-header에 접근할 수 있는 환경에서

쿠키를 컨트롤할 수 있도록 만들어주었다.

 

결과는 성공적이었고,

모든 server - actions / mutations와

클라이언트에서의 fetch 요청으로도 성공적인

로그인 유지가 가능한 결과를 얻을 수 있었다.

 

✍️마치며...

개발에 대한 경험이 늘어남에 따라 느끼는 것은 개발은 언제나 정답이 정해져있는 것이고 그 정답에 도달하기 위해선 내가 맞닥뜨린 문제에서 성공적인 예시를 통해 하나씩 이어가는 것이 가장 빠르고 올바르게 이해하며 정확하게 문제를 해결할 수 있는 방법인 것 같다.

2023. 7. 9. 18:18

getServerSideProps

주요 기능은 특정 페이지의 index에서 해당 컴포넌트가 렌더링 되기 전에

Pre-Rendering(SSR)에 필요한 데이터를 해당 컴포넌트의 props를 통해서

데이터를 전달해 줄 수 있는 기능이다.

 

언제 사용하면 좋을까?

많은 경우가 있겠지만 가장 적합하다고 생각하는 상황은 응답받은 데이터를

html에 모두 포함된 상태로 seo최적화되어 렌더링되어야하고 동적으로 데이터가

자주 변경될 여지가 있고 반드시 최신화된 데이터를 보여주어야 하는 경우이다.

 

예시 소스코드

// pages/video/index.tsx

export interface VideoList {
    id: string;
    title: string;
    publishedAt: string;
    thumbnail: string;
    channelId: string;
    channelTitle: string;
    channelThumbnail: string;
    free: boolean;
    category: string;
}

export const getServerSideProps: GetServerSideProps<{ data: VideoList[] }> = async () => {
    const data: VideoList[] = [];

    const res = await getFirebaseData(); // 파이어베이스 데이터로드
    for (const row of res.data) {
        const youtube: any = await getYoutubeVideoData(row.id); // argument = videoId in FB
        const channelData: any = await getYoutubeChannelData(youtube.snippet.channelId);
        const newData = {
            id: youtube.id,
            title: youtube.snippet.title,
            publishedAt: youtube.snippet.publishedAt,
            thumbnail: youtube.snippet.thumbnails?.medium.url, // data = youtube "video" api
            channelId: youtube.snippet.channelId,
            channelTitle: youtube.snippet.channelTitle,
            channelThumbnail: channelData?.snippet.thumbnails.default?.url, // channelData = youtube "channel" api
            free: row.free, // from firebase
            category: row.category, // from firebase
        }
        data.push(newData);
    }

    return { props: { data } }
}

export default function Home({ data } : InferGetServerSidePropsType<typeof getServerSideProps>) {
    const element = useRef<CustomScroller>(null);

    return (
        <div className='relative w-full h-full overflow-hidden box-border bg-primary-dark-400 '>
            <header className='w-[375px] max-mobile-md:w-full fixed max-mobile-md:top-0 bg-primary-dark-400 z-10'>
                <NavBar />
                <CategoriesList />
            </header>
            <main className='absolute bottom-0 w-full h-[calc(100%-135px)] overflow-hidden'>
                <div className="relative w-full h-full">
                    <CustomScroller ref={element} universal={true}>
                        <MediaList data={data}/>
                        <button
                            type='button'
                            className="fixed bottom-16px right-16px"
                            onClick={() => {
                                if (!element.current) return
                                element.current.scrollTop();
                            }}
                        >
                            <ScrollTopIcon/>
                        </button>
                    </CustomScroller>
                </div>
            </main>
        </div>
    )
}

직접 진행했던 프로젝트에서 사용했던 코드이다. 통신해야 할 부분을 getServerSideProps에서 통신을 모두 마친 후에 메인 컴포넌트인 Home컴포넌트의 props로 데이터를 보내주었고 MediaList컴포넌트의 props로 통신완료된 데이터를 전달해주고있다.

 

Github: https://github.com/zeriong/side-project

 

유의할 점

getServerSideProps를 사용할땐 반드시 해당페이지의 메인 컴포넌트 파일(index.tsx)에서만 사용가능하다.

 

 

마치며...

처음에 ssr기능이 그저 next.js프레임워크를 사용하기만하면 되는 줄 알았는데 전혀 아니였다. 항상 새로운 프레임워크를 사용할땐 공식페이지에서 Document를 꼭 정독하고 프레임워크가 지향하는 개발의 자연스러운 방향과 기능들에 대해 인지하고 적재적소에 사용할 수 있도록 준비해두는 것이 개발자로서 더 올바른 자세라는 것을 깨달았다.

 

2023. 1. 14. 16:02

Next.js 파일(pages) 구조

public 						
├── asset     
      ├── image
      └── video

// 어플리케이션의 기능에 사용되는 소스들
component                                			 
├── common              
└── layout              
      ├── layout.tsx
      ├── header.tsx
      ├── nav.tsx
      └── footer.tsx
		                  
pages			
│                  
├── index.tsx
├── _app.tsx         
│                             
├── _document.tsx
│                      
├── api               
└── product
       └── [id].tsx

styles
├── globals.css             
└── Home.modules.css

pages 내부의 파일명에 따라서 해당 컴포넌트의 path가 정해진다.

pages내부에 index.tsx는 path = " / " 가장 첫화면(home)이고
about.tsx는 path = " /about " 이다.

 

pages => product => [id].tsx 의 경우

path = ' /product/id ' 가 된다.

 

 

_app.js  _document.js

  • 최초로 실행되는 파일들이다
    ( _app.js 는 최초 실행   =>   _document.js 는 _app.js이후실행 )
  • pages 폴더 내에 위치해야 한다.
  • 두 파일 모두 client 에서 사용하는 로직을 적용할 수 없다 -> eventListner 등
    < _app.js >
    전체 컴포넌트의 레이아웃으로 React.js에서 App.js와 같은 역할이다.
    최초에 실행되어 내부에 들어갈 컴포넌트들을
    전부 실행하고 Html의 Body로 구성하여 rendering한다.

 

 

404 page custom

pages 폴더에 404.js를 생성하고 그 곳에 원하는 커스텀을 해주면 된다.

 

사용예시

export default function NotFound() {
return "What are you doing here?"
}

 

 

 

rewrites기능

rewtrites는 말 그대로 다시 쓰는 기능으로, url에서 바뀐내용이 직접적으로 보여지는 것이 아닌url이나 console, network에서 볼 수 없도록 내부적으로만 바뀐 데이터를 받는 기능이다.

 

사용예시

/** @type {import('next').NextConfig} */
const API_KEY = process.env.API_KEY;

const nextConfig = {
  reactStrictMode: false,
  async rewrites(){
    return[
        {
          source: "/api/movies", //변경된 url
          destination: `https://api.themoviedb.org/3/movie/popular?api_key=${API_KEY}` //실행되는 파라미터
        },
      {
        source: "/api/movies/:id",
        destination: `https://api.themoviedb.org/3/movie/:id?api_key=${API_KEY}`
      }
        ];
  }
}

module.exports = nextConfig

 

해당 클라이언트 url에 source에 적힌 파라미터를 넣으면 destination에 할당된 내용으로 보여지지만

url에서 보여지는 파라미터는 source에 적힌 파라미터만 보여지게된다.

 

즉 url에 공유가 되면 안될 내용이 포함되어 있는 경우 rewrite를 사용하여

destination내부의 작성하면 된다. 

 

 

redirect기능

url에서 특정한 파라미터로 접속할 경우 따로 지정해둔 페이지로 연동시켜주는 기능이다.

 

사용예시

/** @type {import('next').NextConfig} */
const API_KEY = process.env.API_KEY;

const nextConfig = {
  reactStrictMode: false,
  async redirects(){
    return [
      {
        source:"/contact/:path*", // :path* 는 /contact/123456 처럼 아무거나써도 해당된다.
        destination:"https://www.google.com/", // 이동될 페이지
        permanent:false,
      }
    ];
  },
}

module.exports = nextConfig

 

마치며...

가독성이 좋은 아키텍쳐를 사용하는 것은 정말 좋다고 생각한다. 하지만 next.js의 서버부담이 크다는 단점은 seo가 필요한 서비스가 아닌경우에는 피하는게 좋다고 생각이 들었다.

 

github : https://github.com/zeriong/Nextjs_intro

2023. 1. 2. 20:57

1. Framework & Library

React.js는 javascript언어만으로도 html의 기능을 사용 가능하도록 만들고

반복되는 컴포넌트를 더 용이하게 사용할수 있도록 만든 Library이다.

그리고 Next.js는 React.js의 framework이다.

 

둘의 가장 명확한 차이점은 응용프로그램흐름의 주도권자 이다.

 

Framework

코드를 작성하는 기본적인 틀을 제공해서 보다 효율적으로 어플리케이션을 만들 수 있도록

소프트웨어 환경을 만들고 응용 프로그램은 프레임워크에 의해 사용된다.

 

Library

어플리케이션을 만들 때 필요한 자원 즉, 매소드(함수)의 모임이며

응용 프로그램이 라이브러리를 사용한다.

 

2. CSR & SSR

React.js는 CSR(Client-Side-Rendering)이다.

CSR은 클라이언트(Front)에서 브라우저에 보여지는 화면을 구성한다.

 

< 장점 >

초기에 로드만 완료되면 이후 렌더링이 빠르고 서버에 요청할 것이 적기때문에

서버의 부담이 적다.

 

< 단점 >

초기 html이 비어있기 때문에 SEO에 좋지않고 초기로드가 오래걸리고

외부 라이브러리가 추가로 필요한 경우도 많다.

 

장단점에 부합하는 " Web Applications "에 적합하다.

 

 

Next.js는 SSR(Server-Side-Rendering)이다.

SSR은 서버(Back)에서 브라우저에 보여지는 화면을 구성한다.

 

< 장점 >

초기 html에 모든 정보가 포함되어 있기 때문에 SEO에 좋고 초기로딩이 빠르다.

 

< 단점 >

서버에서 앱 전체를 미리 렌더링하기 때문에 컴포넌트 로딩이 오래걸리면 유저는

빈 화면을 보게 될 수도 있고 매번 서버에 요청하기 때문에 서버부하가 크다.

페이지를 요청할 때마다 새로고침되어 UX가 다소 떨어지는 편이다.

 

장단점에 부합하는 " Static sites "에 적합하다.