Next.js Dynamic Route란? 

 

App router는 파일 구조로 router를 표현한다. 디렉토리는 path를, page.tsx는 UI를 나타낸다.

즉, 개발할 때 디렉토리명으로 path를 표현해야 한다.

그리고 Next.js는 Static 렌더링이 기본값이다. 이 말인즉슨 빌드 타임에 페이지가 렌더링된다는 뜻이다. 

그런데 path의 일부분이 변동되는 값이라 어떤 값이 들어올 지 정해지지 않았을 때는 path로 미리 url을 표현할 수 없을 것이다.

예를 들어, 유저 정보 페이지의 URL에 유저 id가 포함되어 있다고 한다면, 모든 유저의 id를 미리 정의해서 디렉토리로 생성해놓는 것은 불가능할 것이다. 

이럴 때 Dynamic Route를 사용할 수 있다. 즉 path에 어떤 값이 들어올 지 미리 알 수 없을 때 사용한다.

Dynamic Route 방식은 필요에 따라 3가지로 나눌 수 있다.

 

1. 일반적인 경우-디렉터리명을 대괄호로 감싼다. ex) [slug]

- app/user/[slug]/page.tsx -> /user/nightlybow23

2. 변동되는 path가 여러 개인 경우-디렉터리명을 두 개의 대괄호로 감싼다. ex) [[slug]]

- app/user/[[slug]]/page.tsx -> /user/nightlybow23/2024/may/25

3. 옵셔널하게 해당 path가 포함돼도 되고, 안 돼도 좋을 때-디렉터리명을 두 개의 대괄호로 감싸고, destructing할 때처럼 ...를 붙인다. ex) [[...slug]]

- app/user/[[...slug]]/page.tsx -> /user/nightlybow23, /user/nightlybow23/2013

 

* generateStaticParams은 Static 렌더링 + Dynamic Route 방식을 같이 사용하기 위한 함수이다. 

page.tsx에 컴포넌트 바깥에 export async function generateStaticParams로 시작하는 함수를 작성해야 한다.

아래와 같이 미리 작성해 놓으면 빌드 타임에 서버에서 미리 fetch를 수행한 후 route를 생성해 놓는다.

export async function generateStaticParams() {
    const users = await fetch(SOME_ENDPOINT)
    return users.map(user => ({
    	slug: user.id
    }))
}

 

generateStaticParams은 동일한 API 요청은 memoized되어 딱 한 번만 요청된다는 장점이 있다.

여러 페이지의 generateStaticParams끼리는 물론이고 page 컴포넌트 내에서도 동일한 API 요청을 한다면 중복 요청하지 않는다.

 

위 같은 장점들이 아니더라도 경험상, Dynamic API를 사용하지 않아 기본값인 Static 렌더링이 적용되고 있을 때, Dynamic Route 방식을 사용하려 하면 ([slug]) page.tsx에 generateStaticParams가 없다는 에러가 발생하며 페이지가 제대로 보여지지 않았다.

Static Rendering인 경우에는 반드시 generateStaticParams을 구현해야 함을 잊지 말자. 

 

'프론트엔드 > Nextjs' 카테고리의 다른 글

서버 렌더링  (0) 2025.01.07
Parallel Route  (0) 2025.01.07
1. 프로젝트 생성과 디렉토리 구조  (0) 2023.12.25

Next.js 공식 사이트에서 말하는 서버 렌더링의 장점은 아래와 같다.

 

1. 데이터 페칭 속도: 서버에서 데이터를 요청하기 때문에 클라이언트에서 해야 하는 요청이 줄어들어 시간이 절약된다.

2. 보안: 토큰이나 API 키 같은 정보를 클라이언트에 노출시키지 않고 사용할 수 있다.

3. 캐싱: 서버에서 렌더링된 결과를 캐싱하기 때문에, 유저에게서 요청이 올 때마다 매번 렌더링하지 않아도 된다.

4. 성능: 필요한 자바스크립트 코드의 양을 줄여서 클라이언트 경험에 도움이 된다. 특히 느린 인터넷, 오래된 기기 등에서 효과적이다.

5. 초기 페이지 로딩 및 FCP: 서버에서 미리 렌더링해서 보내주기 때문에 클라이언트가 자바스크립트를 다운로드하고, 파싱하고, 실행할 때까지 기다릴 필요가 없기 때문에 초기 페이지 로딩 및 FCP가 빠르다.

6. SEO: HTML이 미리 만들어져 있기 때문에 검색 엔진 봇이 인덱싱해가기 쉽다.

7. Streaming: Streaming을 사용하면 완성된 전체 페이지를 보여주는 대신, 렌더링 작업을 여러 개로 나눠서 준비되는 것부터 보여줄 수 있다.

 

Next.js의 컴포넌트는 기본적으로 모두 서버 컴포넌트이다. Next.js에서의 서버 컴포넌트 렌더링은 React와 다르지 않은데, 대략 아래와 같은 과정으로 이루어진다.

서버 측:

1. 서버 컴포넌트를 RSC Payload(React Server Component Payload)라는 특별한 형태로 변환한다.

2. 초기 페이지 HTML을 렌더링하기 위해서 서버 컴포넌트의 RSC Payload와 클라이언트 컴포넌트 자바스크립트를 사용한다.

클라이언트 측:

1. 상호작용 불가능한 HTML을 먼저 보여줘서 초기 페이지 로딩을 빠르게 한다.

2. RSC Payload를 사용해서 서버 컴포넌트와 클라이언트 컴포넌트의 관계를 맺어 주고, DOM을 업데이트한다.

3. Javascript를 사용해서 클라이언트 컴포넌트에 hydrate를 해 주어 상호작용이 가능하게 해 준다.

 

Next.js의 서버 렌더링 방식은 세 가지가 있다.

1. Static Rendering

기본값이며, 빌드 타임 혹은 data revalidation 후에 렌더링되고 이를 재사용한다. 개인정보 등이 아니라 누구나 같은 페이지를 보여주어야 할 때 빠르고 유용하다. 결과는 캐시되며 CDN에 넣을 수도 있다. 추후 ISR(Incremental Static Regeneration)을 통해 데이터가 업데이트될 수 있다.

2. Dynamic Rendering

클라이언트가 요청할 때마다 렌더링된다. Dynamic API나 { cache: 'no-store' } 옵션과 함께 fetch를 사용하면 자동으로 Dynamic Rendering으로 바뀐다. Dynamic API에는 connection, draftMode, searchParams, unstable_noCache 등이 있다. Dynamic Rendering이라고 해서 캐싱되지 않는다는 뜻은 아니며, 선택할 수 있다. 보통은 Static Rendering을 사용할 것이냐, Dynamic Rendering을 사용할 것이냐를 개발자가 고민하고 결정할 필요는 없으며 필요에 따라 개발하다보면 Next.js가 알아서 결정해줄 것이다.

3. Streaming

전체 완성된 페이지를 보여주는 것이 아니라 조각조각 잘라 완성되는 대로 클라이언트에게 보여준다. 여러 컴포넌트 중 느린 network call을 하는 컴포넌트가 있다면, 다른 컴포넌트들은 이 느린 컴포넌트와 관계 없이 먼저 보여줌으로써 유저에게 빠르다는 느낌을 줄 수 있다.  app router에서 loading.js와 React Suspence를 사용하면 적용된다. 

 

Static은 속도가 빠르나 개인화된 데이터를 보여주는 데 사용할 수 없고, Dynamic은 속도가 느리나 개인화된 데이터를 보여주는 데 사용할 수 있다. Streaming은 Dynamic Rendering을 써야 하는데 점진적인 렌더링을 적용함으로써 느린 속도를 보완한 것으로 보인다.

 

'프론트엔드 > Nextjs' 카테고리의 다른 글

Dynamic Route  (0) 2025.01.08
Parallel Route  (0) 2025.01.07
1. 프로젝트 생성과 디렉토리 구조  (0) 2023.12.25

Parallel Route (병렬 라우팅)

- 한 페이지 안에, 하나의 레이아웃 안에 여러 작은 페이지를 렌더링하고 싶을 때 사용한다. '여러 작은 페이지'는 <article>처럼 분리 가능한 구획이라고 생각하면 되겠다. Next.js에서는 이것을 'slot'이라고 부른다.

 

- 동시에 렌더링하기 때문에, fetch와 같은 네트워크 콜로 인한 waterfall을 방지할 수 있다. 

ex) 부모 컴포넌트에서 fetch를 호출하면, 자식 컴포넌트들 중 해당 fetch와 관계 없는 컴포넌트들도 부모의 fetch가 끝날 때까지 기다려야 렌더링이 된다. Next.js의 병렬 라우팅을 사용하면 '동시에' 렌더링되기 때문에 컴포넌트들을 관심사에 따라 분리할 수 있다.

 

- 문법은 부모 페이지 하위에 디렉토리를 생성한 후 디렉토리명 앞에 @를 붙인다. 디렉토리 하위에는 page.tsx가 있어야 한다. 부모 컴포넌트 하위에 layout.tsx에 slot을 children와는 별도로 렌더링한다. 

ex) app/dashboard/@user/page.tsx

 

- 위 path에서 @user는 실제 url로 만들어지지는 않는다. => localhost:3000/dashboard로 접근할 수 있다. 

 

- 각 slot에 하위 페이지를 생성할 수 있다. 탭 이동에 용이하다. 생성 방법은 하위에 디렉토리를 만들면 되는 식으로 app router 기본 방식과 동일하다.

 

- default.js 란?

어떤 페이지 안에 A slot과 B slot이 있고 A slot 하위에 a 페이지와 b 페이지를 탭으로 생성했다. 디렉토리 형태는 아래와 같을 것이다.

- parentPage

  - @A

     - a

        page.tsx

     - b

        page.tsx

  - @B

  layout.tsx

  page.tsx

 

정상적(?)인 루트대로 pageParent에 접근하고, A slot에서 b 탭을 클릭하면 b 탭은 'active'하게 된다. 이 때 @B는 별다른 행위를 취할 필요 없이 pageParent에 접근했을 때 렌더링했던 대로 있게 된다.

 

하지만 만약 이 상태에서 새로고침을 하게 되면 어떻게 될까? path는 `parentPage/b`가 된다. 어쨌든 페이지는 `pageParent`이므로 @A와 @B를 모두 렌더링한다. 그런데 path는 `parentPage/b`, 즉 b가 포함되어 있으므로 각 slot은 하위 페이지를 찾아서 렌더링해야 한다. @A는 문제가 없지만 @B에는 b 페이지가 없으므로 문제가 된다. 이 때 default.js를 만들어서 @B 하위에 넣어두면 이럴 때 렌더링할 fallback을 지정해둘 수 있다.

 

 

 

'프론트엔드 > Nextjs' 카테고리의 다른 글

Dynamic Route  (0) 2025.01.08
서버 렌더링  (0) 2025.01.07
1. 프로젝트 생성과 디렉토리 구조  (0) 2023.12.25

nextjs로 프로젝트 생성을 할 수 있는 방법에는 2가지가 있다. 하나는 create-next-app을 사용하는 방법이고, 다른 하나는 직접 수동으로 생성하는 방법이다.

create-next-app은 자동으로 package.json 작성 및 디렉토리 구조를 생성해줄 뿐만 아니라 next.js 개발자들에 의해 유지보수 된다고 하니 이 방법을 추천한다. 

 

npx create-next-app@latest
yarn create next-app
pnpm create next-app
bunx create-next-app

 

npm 외에 다른 패키지 매니저를 쓰고 싶으면 위에 써놓은 대로 해도 되고, npx create-next-app@latest 뒤에 --use-yarn, --use-pnpm 등을 입력하면 된다.

 

그 외에도 여러가지 옵션이 있지만 위에 작성된 명령어까지만 쳐도 터미널에 인터랙티브하게 설정할 수 있는 옵션들이 뜨니까 거기서 원하는대로 설정하면 된다.

 

이번에는 next.js의 샘플을 연습할 것이므로 아래와 같은 명령어를 사용했다.

pnpm create next-app nextjs-dashboard --example "https://github.com/vercel/next-learn/tree/main/dashboard/starter-example"

 

 

pnpm을 사용해서 nextjs-dashboard라는 이름의 프로젝트를 생성하는데, https://github.com/vercel/next-learn/tree/main/dashboard/starter-example 에 있는 샘플을 포크할 것이라는 뜻이다. 

 

이렇게 하면 nextjs-dashboard라는 디렉토리가 하나 생성되고 프로젝트가 만들어진다. 하위 디렉토리도 자동으로 생성되어 있는데, 구조는 아래와 같다.

- /app : 페이지 및 컴포넌트. (예전에는 페이징을 pages라는 디렉토리를 통해서 했지만 버전이 업그레이드되면서 app 방식을 제공)

- /app/lib : 재사용 가능한 함수들, 유틸리티 등의 js를 넣어두는 곳

- /app/ui : 카드 컴포넌트, 테이블 컴포넌트 등 작은 단위의 ui 컴포넌트를 넣어두는 곳

- /public : 정적인 에셋을 넣어두는 곳

- /scrips : 튜토리얼 따라가다보면 나중에 나올 스크립트 작성하는 곳

 

아무튼, 서버를 실행하기 위해서는 npm i && npm run dev를 터미널에 입력하면 된다. 

https://localhost:3000 에 들어갔을 때 이상한 사이트(아직 css가 적용되지 않음)가 나오면 성공!

'프론트엔드 > Nextjs' 카테고리의 다른 글

Dynamic Route  (0) 2025.01.08
서버 렌더링  (0) 2025.01.07
Parallel Route  (0) 2025.01.07

Content-Security-Policy(이하 CSP)를 알고 가기 전에, 먼저 XSS(Cross Site Scripting)을 알아야 한다. XSS의 의미는 '사이트 간 스크립팅'으로, 공격자가 웹사이트에 악의적인 스크립트 코드를 삽입할 수 있는 웹 취약점을 말한다. 이러한 코드는 유저 측에서 실행되고 공격자가 사용자의 세션을 가로채거나(도용), 웹사이트 변조, 악의적인 컨텐츠 삽입, 피싱 공격 등을 가능하게 한다. 

 

CSP는 이러한 XSS를 포함한 특정 유형의 공격 방지에 도움이 되는 보안 계층이다. CSP를 활성화하는 방법은 첫째로 웹서버에서 Content-Security-Policy HTTP 헤더를 반환하게 하는 방법이 있고 두번째 방법은 여기서 알아볼 <meta> 태그를 사용해 정책을 구성하는 것이다.

 

<meta http-equiv="Content-Security-Policy" content="" />

기본 형태는 위와 같다. content="" 안에 "key1 value1; key2 value2; key3 value3;" 형태로 연속적으로 값을 지정해주면 된다. key에 들어갈 수 있는 주요 값들은 https://content-security-policy.com/#source_list 여기에 자세히 나와 있다.

 

 

Reflow란?

브라우저에서 DOM의 위치와 크기를 계산하는 일

 

Layout Thrashing이란?

Reflow는 시간이 오래 걸리는 무거운 작업이므로 브라우저 렌더링 엔진은 Reflow가 발생할 때마다 실행하지 않고 큐에 모아서 어느 순간에 한꺼번에 batch로 처리하는데, getClientBoundingRect(), scrollTop과 같은 함수들이 호출되면 최신 스타일을 반영해서 돌려주기 위해 그동안 모아놨던 큐를 비우면서 전부 처리한다. 브라우저가 성능 향상을 위해 기껏 reflow 발생시키는 애들을 큐에 모아놨는데, 자꾸 scrollTop, innerWidth 등을 호출해버리면 reflow가 그만큼 매번, 자주 일어나게 되어 성능 저하로 이어진다. 왜 저런 프로퍼티, 메소드가 DOM을 변화시키는 것도 아닌데 왜 reflow를 발생시킨다고 하는지 궁금했는데 해결되었다. 

나중에 다시 정리할 것

Debounced async function returns no promise
Debounce return undefined if func return promise
DEBOUNCING AN API CALL WITH PROMISE CHAINING

자바스크립트에는 배열이나 객체를 순회할 수 있는 여러 가지 메소드가 존재한다. 

1. for

사용법

for (var i = 0; i < 반복횟수; i++) {

 ...

}

기능

배열을 순회할 때 사용할 수 있다.

여느 언어의 for문과 마찬가지로 break, continue를 이용해 원하는 조건을 만났을 때 빠져나올 수 있다.

예제

var fruits = ['apple', 'banana', 'carrot', 'mango', 'kiwi'];

for(var i=0; i<fruits.length; i++) {
    if (fruits[i] === 'carrot') {
        console.log('carrot is not a fruit');
        break;
    }
    else {
        console.log(fruits[i], 'is a fruit.');
    }
}

/** 실행결과
apple is a fruit.
banana is a fruit.
carrot is not a fruit. (브레이크됨)
 */

 

 

 

2. forEach

사용법

배열.forEach(function callback(element, index) => {

...

});

기능

배열을 순회해서 하나씩 개체를 꺼내 무언가 일을 수행해야 할 때 사용한다. 배열에서 element를 하나씩 가져와서 콜백함수를 실행한다.

forEach는 무조건 순회를 완료한다. 중간에 빠져나올 수 없다. 'carrot'읆 만나면 return false를 써서 break를 걸려고 해 봤으나 작동하지 않고 kiwi까지 순회했음을 알 수 있다.

예제

    var fruits = ['apple', 'banana', 'carrot', 'mango', 'kiwi'];

    fruits.forEach(f => {
        if (f === 'carrot') {
            console.log('carrot is not a fruit.');
            return false;  // 작동하지 않는다.
        }
        else {
            console.log(f, 'is a fruit.');
            return true;
        }
    });
    
/** 실행결과
apple is a fruit.
banana is a fruit.
carrot is not a fruit.
mango is a fruit.
kiwi is a fruit.
 */

 

 

 

3. map

사용법

배열.map(function callback(element, index) => {

...

}

기능

배열을 순회해서 새로운 배열을 만들어낼 때 사용한다. 원래 배열의 개체들을 하나씩 순회해서 일대일로 결과물을 만들어내는 메소드이기 때문에 애초에 break나 continue가 존재하지 않는다.

예제

    var fruits = ['apple', 'banana', 'carrot', 'mango', 'kiwi'];

    var fruits_sentences = fruits.map(f => {
        if (f === 'carrot') return f + ' is not a fruit.';
        else return f + ' is a fruit.';
    });

    console.log(fruits_sentences);

    /**
     * 
     * [
        'apple is a fruit.',
        'banana is a fruit.',
        'carrot is not a fruit.',
        'mango is a fruit.',
        'kiwi is a fruit.'
        ]
     */

 

 

 

4. some

사용법

배열.some(function callback(element, index) {
  if (찾고자 하는 조건) return true; // 원하는 조건의 개체가 있다면 순회 종료
  else return false; // 없어도 되는 문장
}

기능

배열에 원하는 조건의 개체가 하나라도 존재하는지 찾는다. 콜백함수 내에서 true가 반환되면 some 순회가 종료된다.

예제

    var fruits = ['apple', 'banana', 'carrot', 'mango', 'kiwi'];

    var isExistCarrot = fruits.some(f => {
        if (f === 'carrot') return true;
    });

    console.log('Does a carrot exist?', isExistCarrot);

    // 실행결과
    // Does a carrot exist? true

 

 

 

5. every

사용법

배열.every(function callback(element, index) {
  if (!모두 만족해야 하는 조건) return false;
  else return true;
}

기능

배열의 모든 개체가 조건을 만족하는 지 검사한다. 콜백함수가 false를 리턴하면 조건을 충족시키지 않는다고 판단하고 순회를 종료한다.

예제

    var fruits = ['apple', 'banana', 'carrot', 'mango', 'kiwi'];

    var isAllFruit = fruits.every(f => {
        if (f === 'carrot') return false;
    });

    console.log('Is all a fruit?', isAllFruit);

	// 실행결과
    // Is all a fruit? false

 

 

 

6. filter

사용법

var 결과물 = 배열.filter(function callback(element, index) {
  if (찾고자 하는 조건) return true;
  else return false;
}

기능

조건을 통과하는 개체로만 이루어진 새로운 배열을 만들어낼 때 사용한다. 콜백이 true를 반환하면 해당 개체가 새로운 배열에 추가되고, 아니면 무시된다.

예제

    var fruits = ['apple', 'banana', 'carrot', 'mango', 'kiwi'];

    var onlyFruits = fruits.filter(f => {
        if (f !== 'carrot') return true;
        else return false;
    });

    console.log(onlyFruits);

    // 실행결과
    // [ 'apple', 'banana', 'mango', 'kiwi' ]

 

 

 

7. reduce

사용법

var 결과 = 배열.reduce(function callback(누적값, 현재값) {
  if (조건에 맞으면) return 누적값 + 현재값;
  else return 누적값; // return 누적값 + 0;
}, 초깃값)

기능

배열을 순회해서 원하는 조건을 충족시키는 개체로만 무언가 누적해서 결과를 만들어낼 때 사용한다. 덧셈이라던가...

예제

    var numbers = [1, 2, 3, 4, 5];

    var sumOfOneTwoThreeFour = numbers.reduce((prev, curr) => {
        if (curr < 5) return prev + curr;
        else return prev;
    }, 0);

    console.log(sumOfOneTwoThreeFour);

    // 실행결과
    // 10

+ Recent posts