Next.js: ํ•ฉ๋ฒ•์ ์ธ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ๋งํฌ์˜ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก ํƒ์ƒ‰์—๋Š” ์ž‘๋™ํ•˜์ง€๋งŒ ํ•˜๋“œ ์ƒˆ๋กœ ๊ณ ์นจ(ssr) ์‹œ ๋ฒˆ๋“ค์„ ์ฐพ์„ ์ˆ˜ ์—†๊ณ  404๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.

์— ๋งŒ๋“  2018๋…„ 09์›” 20์ผ  ยท  119์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: vercel/next.js

ํ•ฉ๋ฒ•์ ์ธ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ๋งํฌ์˜ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก ํƒ์ƒ‰์—๋Š” ์ž‘๋™ํ•˜์ง€๋งŒ ํ•˜๋“œ ์ƒˆ๋กœ ๊ณ ์นจ(ssr) ์‹œ ๋ฒˆ๋“ค์„ ์ฐพ์„ ์ˆ˜ ์—†๊ณ  404๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.

๋ฒ„๊ทธ ์‹ ๊ณ 

๋ฒ„๊ทธ ์„ค๋ช…

์ œ๋ชฉ์— ์ถ”๊ฐ€ ์„ค๋ช…์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์•Œ๋ ค์ฃผ์‹ญ์‹œ์˜ค.

๋ชจ๋“  ๊ด€๋ จ ๋ฌธ์ œ ๋Š” 6-์นด๋‚˜๋ฆฌ์•„(๊ทธ๋ ‡์ง€ ์•Š๋‹ค๊ณ  ์ƒ๊ฐํ•จ) ๋˜๋Š” ํ–ฅ์ƒ๋œ ์„œ๋ธŒ(์•„๋งˆ๋„ ํ”„๋กœ๋•์…˜ ์ •์  ๋‚ด๋ณด๋‚ด๊ธฐ์—์„œ๋งŒ ํ•ด๋‹น๋จ)๋กœ ์ˆ˜์ •๋˜์—ˆ๋‹ค๋Š” ์ถ”๋ก ์œผ๋กœ ๋งˆ๊ฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด ๋ธ”๋กœ๊ทธ๋ฅผ next.js๋กœ ๋‹ค์‹œ ์ž‘์„ฑ ์ค‘์ด๋ฉฐ ์ด์ „์— ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‚ด next.js ๊ธฐ๋ฐ˜ ๋ธ”๋กœ๊ทธ๋ฅผ ๊ตฌ์ถ•ํ•˜๋ฉด ์ตœ์‹  serve ๊ฐ€ ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ dev env๋ฅผ ์ˆ˜์ •ํ•˜๋ ค๋ฉด ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  prod์—์„œ 301 Moved Permanently ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜๋Š” dev์—์„œ ๋Š์–ด์ง„ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ ์ง€์›์œผ๋กœ ์‚ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์žฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด

๋‹ค์Œ์€ ์žฌํ˜„ ๊ฐ€๋Šฅํ•œ ์ตœ์†Œ ์‚ฌ๋ก€์ž…๋‹ˆ๋‹ค(repro repo์— ๋Œ€ํ•œ ๋งํฌ๋Š” ์Šค๋‹ˆํŽซ ์•„๋ž˜์— ์žˆ์Œ).

// pages/index.js
import Link from "next/link";

export default () => (
  <Link href="/about/">
    <a>About</a>
  </Link>
);

// pages/index.js
export default () => "about";

์žฌํ˜„ ๊ฐ€๋Šฅํ•œ ์ตœ์†Œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ https://github.com/iamstarkov/next.js-trailing-slash-bug-demo

  1. ๋ณต์ œ ์ €์žฅ์†Œ git clone https://github.com/iamstarkov/next.js-trailing-slash-bug-demo
  2. ๋””๋ ‰ํ† ๋ฆฌ ๋ณ€๊ฒฝ cd next.js-trailing-slash-bug-demo
  3. ์„ค์น˜ deps yarn
  4. ๊ฐœ๋ฐœ์ž ์‹คํ–‰: yarn dev
  5. ์—ด๊ธฐ http://localhost :3000/
  6. devtools์˜ ๋„คํŠธ์›Œํฌ ํƒญ์„ ์—ฝ๋‹ˆ๋‹ค.
  7. http://localhost:3000/_next/static/development/pages/about.js ๊ฐ€ 200์ด ๋˜๋Š” ๊ฒƒ์„ ๊ด€์ฐฐํ•˜๋‹ค
  8. http://localhost:3000/_next/on-demand-entries-ping?page=/about/ ๊ฐ€ 200์ด ๋˜๋Š” ๊ฒƒ์„ ๊ด€์ฐฐํ•˜๋‹ค
  9. http://localhost:3000/about/ ์ด 404ed์ธ ๊ฒƒ์„ ๊ด€์ฐฐํ•˜์‹ญ์‹œ์˜ค.
  10. http://localhost:3000/about/ ํ•ด๊ฒฐ์„ ์œ„ํ•œ ์ง€์†์ ์ธ ์‹œ๋„ ๊ด€์ฐฐ
  11. ํ„ฐ๋ฏธ๋„์—์„œ ๊ด€์ฐฐ Client pings, but there's no entry for page: /about/
  12. ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ
  13. 404 ํŽ˜์ด์ง€๋ฅผ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค.
  14. URL์—์„œ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ http://localhost :3000/about์„ ํด๋ฆญ
  15. ํŽ˜์ด์ง€๊ฐ€ 200ํ™”๋˜๋Š” ๊ฒƒ์„ ๊ด€์ฐฐํ•˜์‹ญ์‹œ์˜ค
  16. ์˜ค๋ฅ˜ ์ง€์†์„ฑ์„ ํ™•์ธํ•˜๋ ค๋ฉด 5-15๋‹จ๊ณ„๋ฅผ ํ•œ ๋ฒˆ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ์ƒ๋˜๋Š” ํ–‰๋™

  1. /about/ ๋Š” 404 not found ๋กœ ํ•ด๊ฒฐ๋˜์–ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค.
  2. /about/ ๋Š” 200 ok ๋กœ ํ•ด๊ฒฐ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  3. ์„œ๋ฒ„๋Š” Client pings, but there's no entry for page: /about/ ์ธ์‡„ํ•˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  4. /about ๋ฐ /about/ ๋ชจ๋‘ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์Šคํฌ๋ฆฐ์ƒท

ํ•ด๋‹น ์—†์Œ

์‹œ์Šคํ…œ ์ •๋ณด

  • OS: macOS ํ•˜์ด ์‹œ์—๋ผ 10.13.6(17G65)
  • ๋ธŒ๋ผ์šฐ์ €(์ค‘์š”ํ•˜์ง€ ์•Š์ง€๋งŒ chrome 69.0.3497.100 ๋ฐ safari ๋ฒ„์ „ 12.0(13606.2.11)์—์„œ ์žฌํ˜„ํ•  ์ˆ˜ ์žˆ์Œ)(safari 11์—์„œ ๋™์ผ)
  • Next.js ๋ฒ„์ „: 7.0.0(5.x ๋ฐ 6.x์—์„œ ์žฌํ˜„ ๊ฐ€๋Šฅ)

์ถ”๊ฐ€ ์ปจํ…์ŠคํŠธ

์—ฌ๊ธฐ์— ๋ฌธ์ œ์— ๋Œ€ํ•œ ๋‹ค๋ฅธ ์ปจํ…์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์‹ญ์‹œ์˜ค.

https://github.com/zeit/next.js/blob/459c1c13d054b37442126889077b7056269eeb35/server/on-demand-entry-handler.js#L242 -L249์—์„œ ์ด ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด

๋˜๋Š” node_modules/next/dist/server/on-demand-entry-handler.js ํ˜„์ง€์—์„œ

          const { query } = parse(req.url, true)
          const page = normalizePage(query.page)
+         console.log('query.page', query.page);
+         console.log('page', page);
+         console.log('Object.keys(entries)', Object.keys(entries));
          const entryInfo = entries[page]

          // If there's no entry.
          // Then it seems like an weird issue.
          if (!entryInfo) {
            const message = `Client pings, but there's no entry for page: ${page}`

next dev ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๊ณ  http://localhost :3000/์„ ์—ด๊ณ  about link๋ฅผ ํด๋ฆญํ•œ ๋‹ค์Œ:

  • /about
    query.page /about page /about Object.keys(entries) [ '/', '/about' ]
  • /about/ :
    query.page /about/ page /about/ Object.keys(entries) [ '/', '/about' ] Client pings, but there's no entry for page: /about/

๋‚˜๋Š” ๋ฌธ์ œ(์ ์–ด๋„ ๊ทธ๊ฒƒ์˜ ์ผ๋ถ€)๋Š” ํŽ˜์ด์ง€์— ์Šฌ๋ž˜์‹œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ onDemandEntryHandler์˜ ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ํ•ญ๋ชฉ์—์„œ ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋‹ค๋Š” ์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

2์‹œ๊ฐ„ ๋™์•ˆ์˜ ์กฐ์‚ฌ์™€ ์ค€๋น„๊ฐ€ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค.

story 8 feature request

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

์šฐ๋ฆฌ๋Š” ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ณง ์ถœ์‹œํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

๋ชจ๋“  119 ๋Œ“๊ธ€

๊ฐ€์žฅ ๊ด€๋ จ์„ฑ์ด ๋†’๊ณ  ์ฃผ๋ชฉํ• ๋งŒํ•œ ๋ฌธ์ œ๋Š” #1189 ๋ฐ #3876์ž…๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๊ฐ€ ๋งˆ์นจ๋‚ด ํ•ด๊ฒฐ๋˜๊ธฐ๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค! @timneutkens Next 7์— ๋Œ€ํ•œ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ ๋ฌธ์ œ์˜ ์ƒํƒœ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

@NathanielHill next@7 ์—์„œ ์žฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ €๋Š” nextjs 7์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฉฐ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋Š” dev์™€ prod ๋ชจ๋‘์—์„œ 404๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

  • ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ
  • ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ ์‹œ

๊ทธ๋ฆฌ๊ณ  ์˜ํ–ฅ:

  • ์™ธ๋ถ€ ๋งํฌ
  • ๋‚ด๋ถ€ ๋งํฌ
  • ๋ธŒ๋ผ์šฐ์ €์— ๋ถ™์—ฌ๋„ฃ์€ URL

ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์ œ๊ฑฐํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค.

ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋Š” ์ข…์ข… ๋ธŒ๋ผ์šฐ์ €, ์„œ๋ฒ„ ๋ฐ/๋˜๋Š” ๋งํฌ๋ฅผ ๋ถ™์—ฌ๋„ฃ์„ ์ˆ˜ ์žˆ๋Š” ๊ธฐํƒ€ ์„œ๋น„์Šค์— ์˜ํ•ด ์ถ”๊ฐ€๋˜๋ฏ€๋กœ ๋‚ด๋ถ€ ๋งํฌ๋Š” ์ œ์–ดํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์™ธ๋ถ€ ์‚ฌ์šฉ์ž๊ฐ€ ๋„๋‹ฌํ•  ์ˆ˜ ์žˆ๋Š” ๋งํฌ๋Š” ์ œ์–ดํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

๋ฒ„์ „ 7์—์„œ๋„ ์ด ๋ฌธ์ œ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๊ด€๋ จ์ด ์žˆ๋Š”์ง€ ํ™•์‹คํ•˜์ง€ ์•Š์ง€๋งŒ ํ•˜๋‚˜์˜ Next.js ํ”„๋กœ์ ํŠธ๋ฅผ ๋‹ค๋ฅธ Now ๋ฐฐํฌ์˜ ํ•˜์œ„ ํด๋”์— ๋ณ„์นญ์œผ๋กœ ์ง€์ •ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๊ธฐ๋ณธ URL์€ primer.style ์ด๊ณ  primer-components.now.sh Next.js ์•ฑ์˜ ๋ณ„์นญ์„ primer.style/components ํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜์—์„œ primer.style/components ์˜ ์ธ๋ฑ์Šค ํŽ˜์ด์ง€๋Š” ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜์ง€๋งŒ primer.style/components/ ๋Š” 404๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ์ฐพ๊ธฐ ์œ„ํ•ด ์•ฝ๊ฐ„์˜ ๊ฒ€์ƒ‰์„ ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. Netlify์—์„œ ์ •์  ๋ฐฐํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ prod์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์ง€๋งŒ ๊ฐœ๋ฐœ(Next 7)์—์„œ๋Š” ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๊ฐ€ ์žˆ์œผ๋ฉด ์ปดํŒŒ์ผ์ด ์ค‘์ง€๋˜๊ณ  ์ด์œ ๋ฅผ ํŒŒ์•…ํ•˜๊ธฐ ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค. ๋‚˜๋Š” ์ด๊ฒƒ์ด (๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Œ) ์ข‹์€ DX๋ผ๊ณ  ์ƒ๊ฐํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ €๋„ ์ด ๋ฌธ์ œ๋ฅผ ๊ฒช๊ณ  ์žˆ๋Š”๋ฐ ์ •๋ง ์งœ์ฆ๋‚˜๋„ค์š”. ๋นจ๋ฆฌ ํ•ด๊ฒฐ๋˜๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค.

ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์›ํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. <Link href='/about' as='/about/'><a>about</a></Link> ํ•˜์ง€๋งŒ ๊ธˆ์š”์ผ/๋‹ค์Œ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ trailingSlash ๋ฅผ ์†Œํ’ˆ์œผ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ํฌํฌ ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋„์›€์ด ๋˜์—ˆ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค

ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์›ํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. <Link href='/about' as='/about/'><a>about</a></Link> ํ•˜์ง€๋งŒ ๊ธˆ์š”์ผ/๋‹ค์Œ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ trailingSlash ๋ฅผ ์†Œํ’ˆ์œผ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ํฌํฌ ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋„์›€์ด ๋˜์—ˆ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค

@aluminick ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๋ฐฉ๊ธˆ ์‹œ๋„ํ–ˆ์ง€๋งŒ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‚˜๋Š” ์—ฌ์ „ํžˆ ์ƒˆ๋กœ ๊ณ ์นจ(ํ˜„์žฌ ๋™์ž‘) ํ›„์— ์ฐพ์„ ์ˆ˜ ์—†๋Š” traling-slashed ํŽ˜์ด์ง€(์ตœ์‹  ๋ฆด๋ฆฌ์Šค)์— ๋„๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

experimental.exportTrailingSlash ๋Š” next export ์ „์šฉ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋„์›€์ด ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

@Janpot ์˜ ์œ ๋งํ•œ pull ์š”์ฒญ #6421์ด

@iamstarkov ์ด ๋ฌธ์ œ์˜ ์ƒํƒœ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? server.js ํ›„ํฌ ์™ธ์— ์†”๋ฃจ์…˜์ด ์žˆ์Šต๋‹ˆ๊นŒ?

@dryleaf ์ƒํƒœ: ์•„์ง ์—ด๋ ค ์žˆ์Šต๋‹ˆ๋‹ค.

์œ ์‚ฌํ•œ ๋ฌธ์ œ... ์Šฌ๋ž˜์‹œ๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ ์ถ”๊ฐ€๋˜๋ฉด ๋ฆฌ๋””๋ ‰์…˜๋ฉ๋‹ˆ๋‹ค. ์˜ˆ: https://github.com/zeit/next.js/////////////issues/5214

GitHub URL์€ ๊ด€๋ จ์ด ์—†์Šต๋‹ˆ๋‹ค.

@iamstarkov ๋ฌด์Šจ ๋ง์ธ์ง€ ์ž˜ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์›๋ž˜ ๊ฒŒ์‹œ๋ฌผ์„ ๋‹ค์‹œ ์ฝ์€ ํ›„ ๋” ๋ช…ํ™•ํ•ด์งˆ ์ˆ˜ ์žˆ์—ˆ๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

GitHub URL์€ ์•ฑ์ด Next.js๋กœ ๋นŒ๋“œ๋  ๋•Œ URL์ด (๊ฐ€๊ธ‰์ ) ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•ด์•ผ ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ๋ฐ๋ชจ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์‚ฌ์šฉ์ž๊ฐ€ ์Šฌ๋ž˜์‹œ๋ฅผ ์ถ”๊ฐ€ํ•ด๋„ URL์€ ๊ณ„์† ์ž‘๋™ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

nextjs 9์— ๋Œ€ํ•œ ์—…๋ฐ์ดํŠธ๊ฐ€ ์žˆ์Šต๋‹ˆ๊นŒ?

์ €๋Š” Next๋ฅผ ์ฒ˜์Œ ์‚ฌ์šฉํ•˜์ง€๋งŒ ์ด ๋ฌธ์ œ์— ๋Œ€ํ•ด ์—ฌ๋Ÿฌ๋ถ„์ด ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

@iamstarkov ์ด ๋ฌธ์ œ์˜ ์ƒํƒœ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

๋‚˜๋Š” ์ด ๋ฌธ์ œ๊ฐ€ ์•ฝ 1๋…„ ๋™์•ˆ ์–ด๋–ค ์‹์œผ๋กœ๋“  ํ•ด๊ฒฐ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ์‚ฌ์‹ค์— ์ถฉ๊ฒฉ์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค!
Next.js ํŒ€์—์„œ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค๋ฅธ ์ด์œ ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๊นŒ?

URL์€ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ์™€ ์ƒ๊ด€์—†์ด ์ž‘๋™ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์›น์—์„œ ์•„๋ฌด ์‚ฌ์ดํŠธ๋‚˜ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค.

์ด๊ฒƒ์ด Next.js์˜ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚œ ๊ฒฝ์šฐ ์ง€๊ธˆ์—์„œ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
Zeit ํŒ€์ด ๋ช‡ ๋…„ ๋™์•ˆ ๊ทธ๋Ÿฌํ•œ ์ค‘์š”ํ•œ ๋ฌธ์ œ๋ฅผ ๋ฌด์‹œํ•œ๋‹ค๋Š” ์‚ฌ์‹ค์ด ์ •๋ง ํ˜ผ๋ž€์Šค๋Ÿฝ์Šต๋‹ˆ๋‹ค.

@exentrich ์ด๊ฒƒ์€ ๋ชจ๋“  ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์Šฌ๋ž˜์‹œ ์—†์ด ๋™์ผํ•œ ๊ฒฝ๋กœ๋กœ ๋ฆฌ๋””๋ ‰์…˜ํ•จ์œผ๋กœ์จ Zeit Now์—์„œ ์‰ฝ๊ฒŒ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

now.json :

"routes": [
    {
      "src": "/(.*)/",
      "status": 301,
      "headers": { "Location": "/$1" }
    },
    ...
]

๊ทธ๋Ÿฌ๋‚˜ ์™œ ์ด๊ฒƒ์ด Next.js ์ž์ฒด์—์„œ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š๊ณ  ํŒ€์ด ์ด ๋ฌธ์ œ๋ฅผ ๋ฌด์‹œํ–ˆ๋Š”์ง€ ์ดํ•ดํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ public/ (์ž‘์—… ์ค‘)์™€ ํ•จ๊ป˜ CRA ๋ณ€ํ™˜์ด ์‹คํ–‰๋˜๋Š” ์ฃผ์š” ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

@rauchg

@NathanielHill ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!
์ด ์†”๋ฃจ์…˜์„ ์‹œ๋„ํ–ˆ์ง€๋งŒ ์ฟผ๋ฆฌ ๋งค๊ฐœ ๋ณ€์ˆ˜๊ฐ€ ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด /some/?query=1 ๋Š” ์ฟผ๋ฆฌ ์—†์ด /some ๋ฆฌ๋””๋ ‰์…˜๋ฉ๋‹ˆ๋‹ค. ๋‹น์‹ ์€ ๊ทธ๊ฒƒ์„ ๊ณ ์น  ๋ฐฉ๋ฒ•์„ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๊นŒ?

์˜ˆ, ๋ฌธ์ œ์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค @exentrich

์ •๊ทœ์‹ ์ฃผ์œ„์— ์•”์‹œ์  ^ ๋ฐ $ ๊ฐ€ ์žˆ๋‹ค๋Š” ๋ง์„ ๋“ค์—ˆ์„ ๋•Œ ๊ทธ ๋™์ž‘์„ ์ถ”์ธกํ•˜์ง€ ๋ชปํ–ˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค(๊ท€ํ•˜์˜ ์˜ˆ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Œ์„ ์˜๋ฏธํ•จ). ๋‹ค์‹œ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•ด ์ž์ฒด์ ์œผ๋กœ ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด์— ์•ก์„ธ์Šคํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.man_shrugging: ํ–‰์šด์„ ๋น•๋‹ˆ๋‹ค

์‚ฌ์šฉ์ž ์ง€์ • ์ต์Šคํ”„๋ ˆ์Šค ์„œ๋ฒ„์™€ avinoamr/connect-slash๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž‘๋™์‹œํ‚ค๋ ค๊ณ  ํ•˜์ง€๋งŒ ๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ ํŠนํžˆ / ๊ฒฝ๋กœ๊ฐ€ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ  SEO(Next์˜ ์ฃผ์š” ๋ฌด์Šน๋ถ€ ์ค‘ ํ•˜๋‚˜)์— ํ”ผํ•ด๋ฅผ ์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ํ™•์‹คํžˆ ํฐ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

301 ๋ฆฌ๋””๋ ‰์…˜ ๋ฐ ์‚ฌ์šฉ์ž ์ง€์ • ์ต์Šคํ”„๋ ˆ์Šค ์„œ๋ฒ„๋Š” ๋ชจ๋‘ ์ˆ˜์ •์ด ์•„๋‹ˆ๋ผ ํ•ดํ‚น์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ œ ๊ฒฝ์šฐ ์—๋Š” ์‚ฌ์šฉ์ž ์ •์˜ Express ์„œ๋ฒ„ ์—†์ด Next์— ์™„๋ฒฝํ•˜๊ฒŒ ์ž‘๋™ํ•˜๋Š” ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์ด ๊ตฌ์ถ•๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋ชจ๋“  ๊ฒƒ์€ ์™„๋ฒฝํ•˜๊ฒŒ ์ž‘๋™ํ•˜์ง€๋งŒ ์ง€๊ธˆ์€ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ ๋ฌธ์ œ ๋•Œ๋ฌธ์— ์ƒˆ Express ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ํ•ดํ‚น์ด๋ผ๋Š” ์ ์„ ๊ฐ์•ˆํ•  ๋•Œ ํ•„์š”ํ•œ ๋…ธ๋ ฅ์ด ๊ณผ๋„ํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋  ์ˆ˜ ์žˆ๋‹ค๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค! ์ด๋Ÿฌํ•œ ์ด์œ  ๋•Œ๋ฌธ์— ๋‚˜๋Š” ์šฐ๋ฆฌ ํŒ€์—์„œ ๋ฐ”๋‹๋ผ React/Angular์™€ ๋ฐ˜๋Œ€๋กœ Next๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค๋Š” ๋ถˆํ‰์„ ๋“ฃ๊ณ  ์žˆ์œผ๋ฉฐ ๋ถ„๋ช…ํžˆ Next์˜ ๊ฒฝ์šฐ๋ฅผ ์•ฝํ™”์‹œํ‚ต๋‹ˆ๋‹ค.

์ถ”์‹ : ์ €๋Š” Next์™€ ์ž‘์—…ํ•˜๋Š” ๊ฒƒ์ด ์ •๋ง ์ข‹์Šต๋‹ˆ๋‹ค โค๏ธ

์ด๊ฒƒ์€ ํŠนํžˆ / ๊ฒฝ๋กœ๊ฐ€ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ  SEO์— ํ”ผํ•ด๋ฅผ ์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ํ™•์‹คํžˆ ํฐ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

๊ทธ๊ฒƒ์€ ๋‹น์‹ ์˜ SEO๋ฅผ ํ•ด์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Google์€ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ์ทจ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค. 404๋กœ ์„ค์ •ํ•ด๋„ ์‚ฌ์ดํŠธ์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋ณด๋‹ค ๋” ์ด์ƒ SEO์— ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋กœ ๋งํฌํ•˜์ง€ ์•Š๋Š” ํ•œ Google์€ ์ฒ˜์Œ๋ถ€ํ„ฐ ํฌ๋กค๋ง์„ ์‹œ๋„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด ๋ฌธ์ œ๋Š” ์—ฌ์ „ํžˆ ์œ ํšจํ•œ ๋ฌธ์ œ์ด์ง€๋งŒ ์—ฌ๋Ÿฌ๋ถ„ ๋ชจ๋‘๊ฐ€ ์ƒ๊ฐํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋œ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

@nik-john @NathanielHill @dkrish @exentrich

301 ๋ฆฌ๋””๋ ‰์…˜์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด Express ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ท€ํ•˜์˜ ์š”๊ตฌ ์‚ฌํ•ญ์— ๋”ฐ๋ผ ๋‹ค๋ฅด์ง€๋งŒ ๋งž์ถคํ˜• server.js ๋‚ด ๊ฒƒ์„ ์ถฉ์กฑ์‹œํ‚ฌ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์Šฌ๋ž˜์‹œ ๋ฐ ๋น„์Šฌ๋ž˜์‹œ ๊ฒฝ๋กœ์— ๋Œ€ํ•ด ์ค‘๋ณต ์ฝ˜ํ…์ธ  ํŽ˜๋„ํ‹ฐ๋ฅผ ๋ฐ›์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— 301 ๋ฆฌ๋””๋ ‰์…˜๋„ SEO์— ๊ฐ€์žฅ ์ ํ•ฉํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

์ €๋Š” โค๏ธ Next.js๋ฅผ ์‚ฌ๋ž‘ํ•˜์ง€๋งŒ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์ง€ ์•Š๊ณ  ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ ํˆฌํ‘œํ•ฉ๋‹ˆ๋‹ค.

// server.js

const { createServer } = require('http');
const { parse } = require("url");
const next = require("next");

const dev = process.env.NODE_ENV !== 'production'
const port = parseInt(process.env.PORT, 10) || 3000;
const app = next({ dev, quiet: false });
const handle = app.getRequestHandler();

(async () => {
    await app.prepare();
    const server = createServer();

    server.on('request', async (req, res) => {

        const parsedUrl = parse(req.url, true);
        const { pathname, query } = parsedUrl;

        if (pathname.length > 1 && pathname.slice(-1) === "/") {
            console.log('server.js - redirect on "/"...', pathname, query);
            const queryString = await Object.keys(query).map(key => key + '=' + query[key]).join('&');
            res.writeHead(301, { Location: pathname.slice(0, -1) + (queryString ? '?'+ queryString : '') });
            res.end();
        }

        handle(req, res, parsedUrl);

    });

    await server.listen(port);
    console.log(`๐Ÿš€ Ready on http://localhost:${port}`);

})();

@์ž”ํŒŸ

๊ทธ๊ฒƒ์€ ๋‹น์‹ ์˜ SEO๋ฅผ ํ•ด์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Google์€ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ์ทจ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค. 404๋กœ ์„ค์ •ํ•ด๋„ ์‚ฌ์ดํŠธ์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋ณด๋‹ค ๋” ์ด์ƒ SEO์— ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋‚˜๋Š” ๊ทธ๊ฒƒ์ด ๋ณธ์งˆ์ ์œผ๋กœ SEO๋ฅผ ํŠน๋ณ„ํžˆ ํ•ด์น˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ ์„ ์ง€์ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ฐœ๋ฐœ์ž๋Š” ๋งค๋ฒˆ ์˜ฌ๋ฐ”๋ฅธ URL ์ •์˜๋ฅผ ์–ป์–ด์•ผ ํ•˜๋ฏ€๋กœ ์‚ฌ๋žŒ์˜ ์‹ค์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Next๋ฅผ ์ฒ˜์Œ ์ ‘ํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋Š” ๋‹ค์Œ(์™„์ „ํžˆ ์ •์ƒ์ ์ธ ๋ชจ์–‘) URL์ด 404 ํŽ˜์ด์ง€๋กœ ์—ฐ๊ฒฐ๋œ๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ์ง€ ๋ชปํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. <Link href='/people/'>

์„ฑ์ˆ™ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ์ด์ƒ์ ์œผ๋กœ๋Š” ๊ทธ๋Ÿฌํ•œ ์ธ์  ์˜ค๋ฅ˜์˜ ๋Œ€์ƒ์ด ๋˜์–ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค.

๊ฒŒ๋‹ค๊ฐ€ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋กœ ๋งํฌํ•˜์ง€ ์•Š๋Š” ํ•œ Google์€ ์ฒ˜์Œ๋ถ€ํ„ฐ ํฌ๋กค๋ง์„ ์‹œ๋„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋‹ค์‹œ - ์‹ค์ˆ˜๋กœ _์— ์—ฐ๊ฒฐํ•˜๋Š” ์‚ฌ๋žŒ์˜ ๋ฌธ์ œ๊ฐ€ ์กด์žฌ www.mysite.com/people/_ ๋Œ€์‹  _์˜ www.mysite.com/people_ (- ์‹ฌ์ง€์–ด ๋Œ€๋ถ€๋ถ„์˜ ๊ฐœ๋ฐœ์ž ์‚ฌ์šฉ์ž๋ฅผ์œ„ํ•œ ์ •ํ™•ํ•˜๊ฒŒ ์ผ์น˜ํ•˜๋Š” ๊ฒƒ ๋‘˜์„).

์ด ๋‘ ๊ฐ€์ง€ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ชจ๋‘ SEO์— _์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค_.

์ด์ œ SEO ์˜ํ–ฅ์„ ๊ณ ๋ คํ•˜์ง€ ์•Š๊ณ  URL์˜ ์˜๋ฏธ๋ก ์  ์˜๋ฏธ๋„ ์žˆ์Šต๋‹ˆ๋‹ค. _does_ _ www.mysite.com/people / _์ด ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ฒƒ์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? ์ด์ƒ์ ์œผ๋กœ๋Š” ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๊ฐ€๋ฆฌํ‚ค๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Next๋Š” pages > people > index.js (_www.mysite.com/people_์˜ ๊ฒฝ์šฐ pages > people.js ์™€ ๋ฐ˜๋Œ€)์— ์žˆ๋Š” ๋ชจ๋“  ๊ฒƒ์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜์ง€๋งŒ ๋Œ€์‹  ์•„๋ฌด ๊ฒƒ๋„ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ผ์šฐํŒ…์ด ์ž‘๋™ํ•˜๋Š” ๋ฐฉ์‹์˜ ๋†’์€ ์ˆ˜์ค€์˜ ๊ฒฐํ•จ.

์ฃผ์š” ๋ผ์šฐํŒ… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—๋Š” ์ด๋ฏธ ์ด์— ๋Œ€ํ•œ ์ผ๋ถ€ ์กฐํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค(์˜ˆ isExact React Router์˜ ๊ฒฝ์šฐ

๋‚˜๋Š” ๋‹น์‹ ์ด ์–ด๋””์—์„œ ์™”๋Š”์ง€ ์ดํ•ดํ•˜์ง€๋งŒ, ์ด๊ฒƒ์€ ์—ฌ์ „ํžˆ โ€‹โ€‹๋ถ€๋”ช์ณ์•ผ ํ•  ๋ˆˆ๋ถ€์‹  ๋ฌธ์ œ๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ next export ์˜ ๊ฒฝ์šฐ์—๋„ ์™„์ „ํžˆ ํ”ผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

์‚ฌ๋žŒ๋“ค์ด ์‹ค์ˆ˜๋กœ ๋งํฌํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค ...

์‚ฌ๋žŒ๋“ค์ด ์šฐ์—ฐํžˆ ์กด์žฌํ•˜์ง€ ์•Š๋Š” URL์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. /some/path/ ๊ฐ€ /some/path/dhgfiuwo ๋ณด๋‹ค ๋œ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

URL์˜ ์˜๋ฏธ๋ก ์  ์˜๋ฏธ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‚ด๊ฐ€ ์•„๋Š” ํ•œ ์ด๊ฒƒ์€ ์˜๋ฏธ๋ก ์  ์ฐจ์ด๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์ง€์‹œํ•˜๋Š” ์‚ฌ์–‘์ด ์—†๋Š” ํ•œ ๋งค์šฐ ์ฃผ๊ด€์ ์ž…๋‹ˆ๋‹ค. URL ์‚ฌ์–‘์— ๋”ฐ๋ฅด๋ฉด ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๊ฐ€ ์žˆ๋Š” ๊ฒƒ๊ณผ ์—†๋Š” ๊ฒƒ์€ ๋‹ค๋ฅธ URL๋กœ ๊ฐ„์ฃผ๋ฉ๋‹ˆ๋‹ค. ๋‚˜๋Š” ์ ์–ด๋„ 7๊ฐ€์ง€ ๋‹ค๋ฅธ ์œ ํšจํ•œ ํ–‰๋™์„ ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ๋‹ค:

  • ์žˆ๋Š” ๊ฒƒ๊ณผ ์—†๋Š” ์™„์ „ํžˆ ๋‹ค๋ฅธ ๋‚ด์šฉ
  • 404์™€ ํ•จ๊ป˜, ํ•ด๊ฒฐ ์—†์ด
  • ๊ฒฐ์˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ์—†๋Š” ๊ฒฝ์šฐ 404
  • ์—†์Œ์œผ๋กœ ๋ฆฌ๋””๋ ‰์…˜
  • ๋กœ ๋ฆฌ๋””๋ ‰์…˜ํ•˜์ง€ ์•Š๊ณ 
  • ๊ฐ€ ์žˆ๋Š” ๊ฒƒ๊ณผ ์—†๋Š” ๊ฒƒ์€ ๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋Š” ํ‘œ์ค€๊ณผ ๋™์ผํ•œ ๋‚ด์šฉ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๊ฐ€ ์žˆ๊ฑฐ๋‚˜ ์—†๋Š” ๋™์ผํ•œ ๋‚ด์šฉ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Œ

/pages/some-page.js ์™€ /pages/some-page/index.js (๋˜๋Š” ๋‘˜ ๋‹ค)๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ๊ฐ€๋Šฅ์„ฑ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค.

next.js๋Š” ์ด๋Ÿฌํ•œ ๋ชจ๋“  ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ์ง€์›ํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ? ๊ธฐ๋ณธ ๋™์ž‘์„ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ?

๋‚˜๋Š” ์ด๊ฒƒ์— ๋ฐ˜๋Œ€ํ•˜์ง€ ์•Š์ง€๋งŒ, ์ „์— ์ด๊ฒƒ์„ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•œ ํ›„์—, ๋‚˜๋Š” ์ฒ˜์Œ์— ๋ณด์ด๋Š” ๊ฒƒ๋ณด๋‹ค ๋” ๋งŽ์€ ๋‰˜์•™์Šค๊ฐ€ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ๋žŒ๋“ค์ด ์šฐ์—ฐํžˆ ์กด์žฌํ•˜์ง€ ์•Š๋Š” URL์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. /some/path/๊ฐ€ /some/path/dhgfiuwo๋ณด๋‹ค ๋œ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

/some/path/dhgfiuwo - ์‚ฌ๋žŒ๋“ค์€ dhgfiuwo ๊ฒฝ๋กœ๊ฐ€ ๋ˆ„๋ฝ๋  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์˜ˆ์ƒํ•ฉ๋‹ˆ๋‹ค. (์˜ˆ๋ฅผ ๋“ค์–ด, dhgfiuwo ์‚ฌ์šฉ์ž๋Š” ์‹œ์Šคํ…œ์—์„œ ์ฐพ์„ ์ˆ˜ ์—†๊ณ  users/dhgfiuwo ์ด ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ์— ์‚ฌ์šฉ์ž๊ฐ€ ์—†๋Š” ๊ฒƒ์€ ์˜ˆ์ƒ๋œ ํ˜„์ƒ์ž…๋‹ˆ๋‹ค.)
/some/path/ ๊ฒฝ์šฐ ์‚ฌ๋žŒ๋“ค์€ ์ด ๊ฒฝ๋กœ๊ฐ€ /some/path ์™€ ๊ฐ™์„ ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋‹ค๋ฅธ ์‚ฌ์ดํŠธ์˜ ๊ธฐ๋ณธ ๋™์ž‘์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
๋”ฐ๋ผ์„œ์—์„œ ์‹คํŒจ would/some/path/ ์ ์€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒƒ๋ณด๋‹ค /some/path/dhgfiuwo .

๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์ด ์ž์‹ ์˜ ์†”๋ฃจ์…˜์„ ๊ฒŒ์‹œํ•œ ๊ฒƒ์„ ๋ณด๊ณ  ์ œ ์ ‘๊ทผ ๋ฐฉ์‹์„ ๊ณต์œ ํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. https://github.com/DevSpeak/next-trailingslash

?=์™€ ๊ด€๋ จํ•˜์—ฌ ๋™์  ๋ผ์šฐํŠธ๋œ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ์ผ๋ถ€ ๊ฐœ์„  ๋ฐ ์ง€์›์€ IMO๋ฅผ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜์ง€๋งŒ ์ด๋Š” ์•„์ด๋””์–ด๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•œ ๊ฒƒ์ผ ๋ฟ์ž…๋‹ˆ๋‹ค.

๋น ๋ฅธ ํ•ด๊ฒฐ์„ ์œ„ํ•ด ๊ธฐ๋ณธ _error ํŽ˜์ด์ง€๋ฅผ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค( @DevSpeak ์˜ ์˜ˆ์—์„œ์™€ ๊ฐ™์ด).

@DevSpeak ,

  • 301 ๋ฆฌ๋””๋ ‰์…˜์„ ํ”ผํ•˜์‹ญ์‹œ์˜ค. ์ด๋Š” ๋ธŒ๋ผ์šฐ์ € ์—
  • errorCode ์‚ผํ•ญ ์„ ์—…๋ฐ์ดํŠธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(์ง€๋‚œ ์ฃผ๊นŒ์ง€ ๋ฌธ์„œ์—์„œ ๊ตฌ์‹์ด์—ˆ์Šต๋‹ˆ๋‹ค).
  • ์ด๊ฒƒ์€ ์„œ๋ฒ„ ์ธก ์ „์šฉ์ด๋ฏ€๋กœ if (typeof window === 'undefined') { ... } ๋กœ ๋ž˜ํ•‘ํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์—์„œ ํŠธ๋ฆฌ ์‰์ดํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์€ Typescript ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค(๋‚ด์žฅ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€ ๊ธฐ๋ฐ˜).

/pages/_error.tsx (๋˜๋Š” TypeScript ์œ ํ˜•์„ ์ œ๊ฑฐํ•˜๊ณ  ์ด๋ฆ„์„ /pages/_error.jsx ):

import React from 'react';
import Head from 'next/head';
import { NextPageContext } from 'next';

const statusCodes: { [code: number]: string } = {
  400: 'Bad Request',
  404: 'This page could not be found',
  405: 'Method Not Allowed',
  500: 'Internal Server Error'
};

export type ErrorProps = {
  statusCode: number;
  title?: string;
};

/**
 * `Error` component used for handling errors.
 */
export default class Error<P = {}> extends React.Component<P & ErrorProps> {
  static displayName = 'ErrorPage';

  static getInitialProps({
    req,
    res,
    err
  }: NextPageContext): Promise<ErrorProps> | ErrorProps {
    const statusCode =
      res && res.statusCode ? res.statusCode : err ? err.statusCode! : 404;
    if (typeof window === 'undefined') {
      /**
       * Workaround for: https://github.com/zeit/next.js/issues/8913#issuecomment-537632531
       * Test vectors:
       * `/test/test/` -> `/test/test`
       * `/test/////test////` -> `/test/test`
       * `/test//test//?a=1&b=2` -> `/test?a=1&b=2`
       * `/test///#test` -> `/test#test`
       */
      const correctPath = (invalidPath: string) =>
        invalidPath
          .replace(/\/+$/, '')
          .replace(/\/+#/, '#')
          .replace(/\/+\?/, '?')
          .replace(/\/+/g, '/');
      if (req && res && req.url && correctPath(req.url) !== req.url) {
        res.writeHead(302, {
          Location: correctPath(req.url)
        });
        res.end();
      }
      const reqInfo = req
        ? `; Url: ${req.url}; IP: ${req.headers['x-forwarded-for'] ||
            (req.connection && req.connection.remoteAddress)};`
        : '';
      console.log(`Error rendered: ${statusCode}${reqInfo}`);
    }
    return { statusCode };
  }

  render() {
    const { statusCode } = this.props;
    const title =
      this.props.title ||
      statusCodes[statusCode] ||
      'An unexpected error has occurred';

    return (
      <div style={styles.error}>
        <Head>
          <title>
            {statusCode}: {title}
          </title>
        </Head>
        <div>
          <style dangerouslySetInnerHTML={{ __html: 'body { margin: 0 }' }} />
          {statusCode ? <h1 style={styles.h1}>{statusCode}</h1> : null}
          <div style={styles.desc}>
            <h2 style={styles.h2}>{title}.</h2>
          </div>
        </div>
      </div>
    );
  }
}

const styles: { [k: string]: React.CSSProperties } = {
  error: {
    color: '#000',
    background: '#fff',
    fontFamily:
      '-apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", "Fira Sans", Avenir, "Helvetica Neue", "Lucida Grande", sans-serif',
    height: '100vh',
    textAlign: 'center',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center'
  },

  desc: {
    display: 'inline-block',
    textAlign: 'left',
    lineHeight: '49px',
    height: '49px',
    verticalAlign: 'middle'
  },

  h1: {
    display: 'inline-block',
    borderRight: '1px solid rgba(0, 0, 0,.3)',
    margin: 0,
    marginRight: '20px',
    padding: '10px 23px 10px 0',
    fontSize: '24px',
    fontWeight: 500,
    verticalAlign: 'top'
  },

  h2: {
    fontSize: '14px',
    fontWeight: 'normal',
    lineHeight: 'inherit',
    margin: 0,
    padding: 0
  }
};

์ฐธ๊ณ ๋กœ ์ด๋Š” ํŽ˜์ด์ง€ ์กฐํšŒ ์‹œ ์˜ค๋ฅ˜๋„ ๊ธฐ๋กํ•˜๋ฏ€๋กœ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•˜์—ฌ ๋งํฌ/๊ธฐํƒ€ ๋ฌธ์ œ๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@DevSpeak @bitjson ์ œ์•ˆ _error.jsx ๊ฐ€ ์›๋ž˜ ํ•˜์šฐ์Šค ๋ผ์šฐํŒ… ๋กœ์ง์ด ์•„๋‹ˆ๋ผ _errors_๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ฒƒ์ž„์„ ๊ณ ๋ คํ•˜๋ฉด ์ œ ์ƒ๊ฐ์—๋Š” ์ด ๋ชจ๋“  ์ฝ”๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฒƒ์ด ํ•ดํ‚คํ•˜๊ณ  ๋งค์šฐ ์„ ์–ธ์ ์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ์‚ฌ์šฉ์ž๊ฐ€ ๋ชจ๋“  ์ฝ”๋“œ ๊ธฐ๋ฐ˜์—์„œ ์ด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€ํ•˜๋Š” ๊ฒƒ์€ ํ•„์ˆ˜ ์‚ฌํ•ญ์ด ์•„๋‹ˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณต๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. = ๋‚˜๋Š” ์ด ์กฐ๊ฑด์ด ๋ผ์šฐํŒ… ๋กœ์ง๊ณผ ํ•จ๊ป˜ ๋‚ด์žฅ๋˜์–ด์•ผ ํ•˜๊ณ  React Router์ฒ˜๋Ÿผ ์˜ตํŠธ์•„์›ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

@๋‚˜๋‹ค๋‹ˆ์—˜ํž

์ฐจ๊ธฐ ์ˆ˜์ถœ์˜ ๊ฒฝ์šฐ์—๋„ ์ด๋Š” ๋ถˆ๊ฐ€ํ”ผํ•˜๋‹ค.

์ž ๊น - ๋ฌธ์„œ ๋ฅผ ์ฝ๊ณ  ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ ์กฐ๊ฑด์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํŠน์ • ์ฝ”๋“œ๊ฐ€ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์ดํ•ดํ–ˆ์Šต๋‹ˆ๋‹ค.

ํŽ˜์ด์ง€๋Š” html ํŒŒ์ผ๋กœ ๋‚ด๋ณด๋‚ด์ง‘๋‹ˆ๋‹ค. ์ฆ‰, /about์€ /about.html์ด ๋ฉ๋‹ˆ๋‹ค.

ํŽ˜์ด์ง€๋ฅผ index.html ํŒŒ์ผ๋กœ ๋‚ด๋ณด๋‚ด๊ณ  ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๊ฐ€ ํ•„์š”ํ•˜๋„๋ก Next.js๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด /about์€ /about/index.html์ด ๋˜๊ณ  /about/์„ ํ†ตํ•ด ๋ผ์šฐํŒ…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ Next.js 9 ์ด์ „์˜ ๊ธฐ๋ณธ ๋™์ž‘์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ next.config.js๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด ๋™์ž‘์œผ๋กœ ๋‹ค์‹œ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// next.config.js
module.exports = {
  exportTrailingSlash: true,
}

์ด๊ฒƒ์ด next export ๋ฅผ ํ†ตํ•œ ์ •์  HTML ๋‚ด๋ณด๋‚ด๊ธฐ์— ๋Œ€ํ•œ ์˜ต์…˜์ด ์•„๋‹ˆ๋”๋ผ๋„ Next๊ฐ€ ์ด (๋†€๋ผ์šด) ๊ธฐ๋Šฅ์„ ์ง€์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋ชจ๋“œ๊ฐ€ ์–ด๋ ค์›€์„ ๊ฒช์„ ํ•„์š”๊ฐ€ ์žˆ๋‹ค๋Š” ๋…ผ๋ฆฌ์— ๋™์˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ ํ†ต๊ณ„๋ฅผ ์•Œ๊ณ  ์žˆ์ง€๋งŒ ๋” ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์ด ์„œ๋ฒ„๋ฆฌ์Šค๊ฐ€ ์•„๋‹Œ ์ผ๋ฐ˜ ์„œ๋ฒ„ ํฌํ•จ ๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค) ํŠนํžˆ ์ด๊ฒƒ์ด ์ผ๋ฐ˜์ ์ธ ์‚ฌ์šฉ ์‚ฌ๋ก€๋กœ ์•Œ๋ ค์ง„ ๊ฒฝ์šฐ

์ฐธ๊ณ : ๊ด€์‹ฌ์„ ๊ฐ€์งˆ๋งŒํ•œ RFC๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. https://github.com/zeit/next.js/issues/9081

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: "/:path*/",
        destination: "/:path",
        statusCode: 301
      }
    ];
  }
};

@Janpot ์‚ฌ๋ž‘ํ•ฉ๋‹ˆ๋‹ค - ์ด๊ฒƒ์€ ์šฐ๋ฆฌ์—๊ฒŒ ์ ˆ๋ฐ˜์„ ๊ฐ€์ ธ๋‹ค ์ค„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฆ‰, ์‚ฌ์šฉ์ž ์ •์˜ ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๊ณ ๋„ ๋ฆฌ๋””๋ ‰์…˜์— ๋Œ€ํ•œ ์ผ์ข…์˜ ์ง€์›์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ถ”๊ฐ€ํ•˜๋Š” ๋ชจ๋“  ๊ฒฝ๋กœ์— ๋Œ€ํ•ด next.config.js ์—์„œ ๋ฆฌ๋””๋ ‰์…˜์„ ์„ค์ •ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ์ „ํžˆ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. ๋˜๋Š” ์–ธ๊ธ‰๋œ @bitjson ๊ณผ ๊ฐ™์€ ๋ชจ๋“  ๊ฒฝ์šฐ๋ฅผ ํฌ์ฐฉํ•˜๊ธฐ ์œ„ํ•ด ์ •๊ทœ์‹์„ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

          .replace(/\/+$/, '')
          .replace(/\/+#/, '#')
          .replace(/\/+\?/, '?')
          .replace(/\/+/g, '/')

๋‘ ๊ฒฝ์šฐ ๋ชจ๋‘ ํ•ต์‹ฌ ํŒ€์ด ์ด RFC์˜ ์šฐ์„  ์ˆœ์œ„๋ฅผ ์ •ํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ํ•œ ๋‹จ๊ณ„ ๋” ๋‚˜์•„๊ฐ€ ์ด๋ฅผ _์„ ํƒ ํ•ด์ œ_ํ•  ์ˆ˜ ์žˆ๋Š”

// next.config.js
module.exports = {
  ignoreStrictRoutes: false, // default value: true
};

๊ฒฐ๋ก ์ ์œผ๋กœ ์ด๊ฒƒ์€ ๋Œ€๋‹จํ•œ ๋ฐœ์ „์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. @Timer!! ๐Ÿ”ฅ

@nik-john "/:path*/" ์—์„œ ์ง€์ •ํ•œ ๊ฒฝ๋กœ๋Š” ๋ชจ๋‘ catchํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค( :path ๋Š” ๋‹จ์ผ ์„ธ๊ทธ๋จผํŠธ๋ฅผ catchํ•˜๊ณ  * ๋Š” 0์—์„œ n๊ฐœ์˜ ์ธ์Šคํ„ด์Šค๋ฅผ catchํ•ฉ๋‹ˆ๋‹ค.)

@Janpot ์•„ ๋ง™์†Œ์‚ฌ ๐Ÿคฆโ€โ™‚ ๊ทธ ์ •๊ทœ์‹์—์„œ ํ›„ํ–‰ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜๋„ ๊ณ ๋ คํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์•„์š”

๋˜ํ•œ ๋‚˜๋Š” ์—ฌ์ „ํžˆ ๋‘ ๋ฒˆ์งธ ๋ถ€๋ถ„์„ ์ง€์ง€ํ•ฉ๋‹ˆ๋‹ค.

๋‘ ๊ฒฝ์šฐ ๋ชจ๋‘ ํ•ต์‹ฌ ํŒ€์ด ์ด RFC์˜ ์šฐ์„  ์ˆœ์œ„๋ฅผ ์ง€์ •ํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ํ•œ ๋‹จ๊ณ„ ๋” ๋‚˜์•„๊ฐ€์„œ ์˜ตํŠธ์•„์›ƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ณธ ์ œ๊ณต ๊ตฌ์„ฑ์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

// next.config.js
module.exports = {
  ignoreStrictRoutes: false, // default value: true
};

์‚ฌ์šฉ์ž ์ง€์ • ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉ ์ค‘์ด๊ณ  ์—„๊ฒฉํ•œ ๊ฒฝ๋กœ๋ฅผ ๋ฌด์‹œํ•˜๋ ค๋Š” ๊ฒฝ์šฐ ๋ฆฌ๋””๋ ‰์…˜ ๋Œ€์‹  ์‚ฌ์šฉ์ž ์ง€์ • ๊ฒฝ๋กœ ์ฒ˜๋ฆฌ๊ธฐ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

app.render(req, res, urlWithoutTrailingSlash, query);

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด /path ๋ฐ /path/ ๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•˜๊ณ  ๋™์ผํ•œ ํŽ˜์ด์ง€๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Oauth ํŽ˜๋”๋ ˆ์ด์…˜ ๊ณต๊ธ‰์ž๋Š” ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์œผ๋ฏ€๋กœ ์ด ๋™์ž‘์€ ๊ฐ„๋‹จํ•œ ํ๋ฆ„์„ ๋งค์šฐ ๋ณต์žกํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ด ๋™์ž‘์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ๊ธฐ์ˆ ์ ์ธ ๋ฌธ์ œ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? ์•„๋‹ˆ๋ฉด ์ด๊ฒƒ์€ ๋‹ค์Œ๋ถ€ํ„ฐ์˜ ๋””์ž์ธ ๊ฒฐ์ •์ž…๋‹ˆ๊นŒ?

์ง€๊ธˆ๊นŒ์ง€ ์ด ์Šค๋ ˆ๋“œ์—์„œ ์–ธ๊ธ‰๋œ ๊ฒƒ์„ ๋ณด์ง€ ๋ชปํ–ˆ์ง€๋งŒ Now๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐฐํฌ ํ›„์— ์ด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. now dev ํ…Œ์ŠคํŠธํ•  ๋•Œ ๋กœ์ปฌ์—์„œ๋งŒ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

const removeTrailingSlashes = (req, res, expressNext) => {
  if (req.path.substr(-1) === '/' && req.path.length > 1) {
    const query = req.url.slice(req.path.length);
    res.redirect(301, req.path.slice(0, -1) + query);
  } else {
    expressNext();
  }
};

stackoverflow์—์„œ ์ด๊ฒƒ์„ ์–ป์—ˆ๊ณ  ์™„๋ฒฝํ•˜๊ฒŒ ์ž‘๋™ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ์†”๋ฃจ์…˜์€ ์ต์Šคํ”„๋ ˆ์Šค์™€ ํ•จ๊ป˜ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

@GaneshKathar Express๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” Next.js๋ฅผ ๊ณ ๋ คํ•˜๋ฉด ์ด๊ฒƒ์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค.

๋‚˜๋Š” ์šฐ๋ฆฌ๊ฐ€ ์ด๊ฒƒ์— ๋™์˜ํ•  ์ˆ˜ ์—†์œผ๋ฉฐ ๊ตฌ์„ฑํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๋‚˜๋Š” ์‹ค์ œ๋กœ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ํ•ญ์ƒ ์›ํ•ฉ๋‹ˆ๋‹ค. ์ƒ๋Œ€ URL์€ ๋ชจ๋“  ํŽ˜์ด์ง€๊ฐ€ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋กœ ๋๋‚  ๋•Œ ์ถ”๋ก ํ•˜๊ธฐ๊ฐ€ ๋” ์‰ฝ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด์ด ์–ด๋–ค ๊ฒƒ์„ ์˜๋ฏธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค /about/index.tsx ์žˆ๋‹ค /about ๋Œ€์‹  /about/ ์Šฌ๋ž˜์‰ฌ์—†์ด ์ง€๊ธˆ ๋‹ค์Œ์ด ๊ธฐ๋Œ€๋ฅผํ•˜์ง€๋งŒ, ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ํŽ˜์ด์ง€๊ฐ€ ์Šฌ๋ž˜์‹œ๋กœ ๋๋‚˜์•ผ ํŽ˜์ด์ง€์— ํ•˜์œ„ ํŽ˜์ด์ง€๊ฐ€ ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ํŽ˜์ด์ง€์— ๋Œ€ํ•ด ๋” ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ฐฉ๋ฒ•์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

/about/index.tsx ํŒŒ์ผ ๋‚ด๋ถ€์— ์ƒ๋Œ€ ๋งํฌ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์€ ์ด์ œ ๋ฒˆ๊ฑฐ๋กญ์Šต๋‹ˆ๋‹ค. ./mysubpage/ ๋งํฌ๋ฅผ ๋งŒ๋“ค๋ฉด ๋Œ€์‹  ์‚ฌ์ดํŠธ์˜ ๋ฃจํŠธ๋ฅผ ๊ฐ€๋ฆฌํ‚ต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ•˜์œ„ ํŽ˜์ด์ง€์˜ ์ด๋ฆ„์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. /about/ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ด๋ฆ„๋งŒ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€๋กœ ๊ฐ€๋“ ์ฑ„์šธ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ƒ๋Œ€ ๋งํฌ๋„ ํŽธ์ง‘ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๋˜ํ•œ wget -r ์‚ฌ์ดํŠธ๋Š” ํ•ญ์ƒ ์Šฌ๋ž˜์‹œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ index.html ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋Š” ํ•ฉ๋ฆฌ์ ์ธ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ด ์„ค์ •์„ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ชจ๋“  ์‚ฌ์ดํŠธ์—์„œ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋ฏ€๋กœ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ํฌ๊ฒŒ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.

๋ฒ„์ „ 9๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๋ฐ ์ด ๋ฌธ์ œ๊ฐ€ ์—ฌ์ „ํžˆ ํ•ด๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

๋‚ด next.config.js ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒƒ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž‘๋™ํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

exportPathMap: async function() {
  const paths = {
    '/': { page: '/' },
    '/authors/index.html': { page: '/authors' },
  };

  return paths;
},

/authors ์•ก์„ธ์Šคํ•˜๋ฉด location ๊ฐ€ /authors/ ๊ฐ€๋ฆฌํ‚ค๋Š” 302๊ฐ€ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค. http-serve ํ…Œ์ŠคํŠธํ•˜๊ณ  ์žˆ๋Š”๋ฐ ์ด ๋™์ž‘์ด ์„œ๋ฒ„ ์ง€์ •์ธ์ง€ ํ™•์‹คํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ์— ์ง๋ฉดํ–ˆ์„ ๋•Œ ๋‚˜๋Š”์ด ์†”๋ฃจ์…˜์„ ์ƒ๊ฐํ•ด ๋ƒˆ์Šต๋‹ˆ๋‹ค.

๋‚ด _error.js ํŽ˜์ด์ง€์—์„œ

Error.getInitialProps = ({ res, err, asPath }) => {
    const statusCode = res ? res.statusCode : err ? err.statusCode : 404;

    const checkForTrailingSlashes = () => {
        if (asPath.match(/\/$/)) { // check if the path ends with trailing slash
            const withoutTrailingSlash = asPath.substr(0, asPath.length - 1);
            if (res) {
                res.writeHead(302, {
                    Location: withoutTrailingSlash
                })
                res.end()
            } else {
                Router.push(withoutTrailingSlash)
            }
        }
    }

    if (statusCode && statusCode === 404) {
        checkForTrailingSlashes();
    } else {
        // 
    }
    return { statusCode };
}

๋ฌธ์ œ๋ฅผ ๊ทน๋ณตํ•˜๋Š” ์ข‹์€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๊นŒ?

์ด๊ฒƒ์€ ์–ด๋–ค๊ฐ€์š”?

ํŽ˜์ด์ง€/_app.jsx

```'react'์—์„œ React ๊ฐ€์ ธ์˜ค๊ธฐ;
'next/app'์—์„œ ์•ฑ ๊ฐ€์ ธ์˜ค๊ธฐ;

๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋ณธ ํด๋ž˜์Šค MyApp ํ™•์žฅ ์•ฑ {
๋ Œ๋”๋ง() {
const { ๊ตฌ์„ฑ ์š”์†Œ, pageProps, ๋ผ์šฐํ„ฐ: { asPath } } = this.props;

// Next.js currently does not allow trailing slash in a route.
// This is a client side redirect in case trailing slash occurs.
if (asPath.length > 1 && asPath.endsWith('/')) {
  const urlWithoutEndingSlash = asPath.replace(/\/*$/gim, '');

  if (typeof window !== 'undefined') {
    window.location.replace(urlWithoutEndingSlash);
  }
  return null;
}

return <Component {...pageProps} />;

}
}
```

@cnblackxp ์ œ์•ˆ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๊ฒŒ ๋‚˜์—๊ฒŒ ๋„์›€์ด ๋˜์—ˆ๋‹ค. ๋‹ค์Œ์€ ๋น„ ํ›„ํ–‰ 404์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ๋™์ž‘์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌํ˜„ํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค(์ฆ‰, ๊ธฐ๋ณธ Error ๊ตฌํ˜„์„ ๋‹ค์‹œ ๋‚ด๋ณด๋‚ด๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค).

import Error from "next/error";
import Router from "next/router";

export default Error;

Error.getInitialProps = ({ res, err, asPath }) => {
  const statusCode = res ? res.statusCode : err ? err.statusCode : 404;

  if (statusCode && statusCode === 404) {
    if (asPath.match(/\/$/)) {
      const withoutTrailingSlash = asPath.substr(0, asPath.length - 1);
      if (res) {
        res.writeHead(302, {
          Location: withoutTrailingSlash
        });
        res.end();
      } else {
        Router.push(withoutTrailingSlash);
      }
    }
  }

  return { statusCode };
};

์˜ˆ, ๋‹ค๋ฅธ ๊ฒƒ์ด ๊ฒฐ์ •๋˜์ง€ ์•Š๋Š” ํ•œ @cansin ์„ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค :) ๊ฑด๋ฐฐ!

@AlexSapoznikov ์˜ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ

  render() {
    const { Component, pageProps, router: { asPath } } = this.props;

    // Next.js currently does not allow trailing slash in a route.
    // This is a client side redirect in case trailing slash occurs.
    if (pageProps.statusCode === 404 && asPath.length > 1 && asPath.endsWith('/')) {

์—ฌ๊ธฐ์„œ ์œ ์ผํ•œ ์ฐจ์ด์ ์€ ์ƒํƒœ ์ฝ”๋“œ๊ฐ€ 404์ธ์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ฆฌ๋””๋ ‰์…˜์œผ๋กœ ์ธํ•ด ํ•ญ์ƒ ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง๋˜๋Š” ๋™์  ๊ฒฝ๋กœ์— ๋Œ€ํ•ด Link๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ธก ๋ผ์šฐํŒ…์ด ์ž‘๋™ํ•˜๋„๋ก ํ•˜๋ ค๋ฉด href ๋งํฌ ์†Œํ’ˆ์— ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์—†์ง€๋งŒ ์ด ๊ฒฝ์šฐ ๋ฆฌ๋””๋ ‰์…˜ํ•˜์ง€ ์•Š๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์˜ค๋ฅ˜ ๊ตฌ์„ฑ ์š”์†Œ์—์„œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ๊ตฌํ˜„ํ•  ๋•Œ์˜ ๋ฌธ์ œ๋Š” ๊ฐœ๋ฐœ ์ค‘์— ์•Œ๋ฆผ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ๋‚˜๋ฅผ ๊ท€์ฐฎ๊ฒŒ ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด์ „ ํด๋ผ์ด์–ธํŠธ ์ธก ๋ฆฌ๋””๋ ‰์…˜์— ๋Œ€ํ•œ ๋ช‡ ๊ฐ€์ง€ ๊ฐœ์„  ์‚ฌํ•ญ:

๊ฐœ์„ ๋œ ์ ์€ ์ด์ œ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๋‹ค์Œ/๋ผ์šฐํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  URL ๊ต์ฒด๊ฐ€ ๋‹ค์‹œ ๋กœ๋“œ ์—†์ด ๋ฐœ์ƒํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํŽ˜์ด์ง€/_app.jsx

import App from 'next/app';
import Router from 'next/router';

export default class MyApp extends App {
  render() {
    const { Component, pageProps, router: { asPath, route } } = this.props;

    // Next.js currently does not allow trailing slash in a route.
    // This is a client side redirect in case trailing slash occurs.
    if (pageProps.statusCode === 404 && asPath.length > 1 && asPath.endsWith('/')) {
      const routeWithoutEndingSlash = route.replace(/\/*$/gim, '');
      const asPathWithoutEndingSlash = asPath.replace(/\/*$/gim, '');

      if (typeof window !== 'undefined') {
        Router.replace(routeWithoutEndingSlash, asPathWithoutEndingSlash);
      }
      return null;
    }

    return <Component {...pageProps} />;
  }
}

404 ์ˆ˜์ •์„ ์œ„ํ•ด @mbrowne ์—๊ฒŒ๋„ ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค :)

@cansin ์˜ ์†”๋ฃจ์…˜์„

MyError.getInitialProps = async ({ res, err, asPath }) => {
  // Capture 404 of pages with traling slash and redirect them
  const statusCode = res 
    ? res.statusCode
    : (err ? err.statusCode : 404);

  if (statusCode && statusCode === 404) {
    const [path, query = ''] = asPath.split('?');                                                                                                                                                                                             
    if (path.match(/\/$/)) {
      const withoutTrailingSlash = path.substr(0, path.length - 1); 
      if (res) {
        res.writeHead(302, {
          Location: `${withoutTrailingSlash}${query ? `?${query}` : ''}`,
        }); 
        res.end();
      } else {
        Router.push(`${withoutTrailingSlash}${query ? `?${query}` : ''}`);
      }   
    }   
  }

@pinpointcoder ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ์™€ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ๋™์‹œ์— ๋ฐœ์ƒํ•˜๋Š” URL์˜ ์˜ˆ๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? /blog/?123 ๋ผ์ธ์„ ๋”ฐ๋ผ ์ƒ๊ฐํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๊นŒ?

์œ„์˜ ๋ช‡ ๊ฐ€์ง€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๋ชจ๋‘์—๊ฒŒ ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ๊ทธ๋“ค์€ ์ผ ํ–ˆ์–ด!

๊ทธ๋Ÿฌ๋‚˜ Next์˜ ํŒ€์—์„œ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ณต์‹์ ์ธ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๊นŒ? ์ด ๋ฌธ์ œ๋Š” ๋ช‡ ๋…„ ๋™์•ˆ ์—ฌ๊ธฐ์— ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋””๋ ‰ํ† ๋ฆฌ ํŽ˜์ด์ง€๋Š” ๋‹ค์Œ ๋‚ด๋ณด๋‚ด๊ธฐ์—์„œ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ์™€ ํ•จ๊ป˜ ์ œ๊ณต๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

@pinpointcoder ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ์™€ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ๋™์‹œ์— ๋ฐœ์ƒํ•˜๋Š” URL์˜ ์˜ˆ๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? /blog/?123 ๋ผ์ธ์„ ๋”ฐ๋ผ ์ƒ๊ฐํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๊นŒ?

@coodoo ๊ทธ ์‚ฌ๋žŒ์€ ์•„๋‹ˆ์ง€๋งŒ, ๋ถˆํ–‰ํžˆ๋„ ์ด๋Ÿฐ ์ผ์ด ๋งŽ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ €๋Š” ํ˜„์žฌ WordPress ์‚ฌ์ดํŠธ๋ฅผ Next.js๋กœ ์ ์ง„์ ์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๋Š” ๊ณผ์ •์— ์žˆ์œผ๋ฉฐ ์–ด๋–ค ์ด์œ ๋กœ ์›๋ž˜ "๊ฐœ๋ฐœ์ž"๋Š” ๋ชจ๋“  ๋‹จ์ผ URL์— ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์ ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ํ˜„์žฌ ํ›„ํ–‰ ๋‘ ๊ฐ€์ง€ ๋ชจ๋‘์— ๋Œ€ํ•œ ์ˆ˜๋งŽ์€ ์š”์ฒญ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์Šฌ๋ž˜์‹œ AND ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜.

ํ‘œ์ค€ URL์— ํ˜„์žฌ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๊ฐ€ ํฌํ•จ๋œ ์ˆ˜๋งŽ์€ ๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๋ฌผ์„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๋ ค๊ณ  ํ•˜๋ฏ€๋กœ ์ง€๊ธˆ ๋‹น์žฅ์€ ์ด๊ฒƒ์ด ํฐ ๊ณ ํ†ต์ž…๋‹ˆ๋‹ค.

๋‚˜๋Š” ์ด๊ฒƒ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ์ž ์ •์˜ ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์œผ๋ฉฐ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์‰ฝ๊ณ  next.js์˜ ํŒŒ์ผ ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ… ์‹œ์Šคํ…œ์„ ๊ณ„์† ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด next.js์— ํ‘œ์‹œ๋˜๋Š” URL์„ ๋‹ค์‹œ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์‹ค์ œ URL์—๋Š” ์—ฌ์ „ํžˆ ๋์— ์Šฌ๋ž˜์‹œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const conf = require('./next.config.js')

const PORT = process.env.PORT || 5000

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev, conf })
const handle = app.getRequestHandler()

app.prepare().then(() => {
    createServer((req, res) => {
        // If there is a slash at the end of the URL, remove it before sending it to the handle() function.
        // This is a workaround for https://github.com/zeit/next.js/issues/5214
        const url =
            req.url !== '/' && req.url.endsWith('/')
                ? req.url.slice(0, -1)
                : req.url
        // Be sure to pass `true` as the second argument to `url.parse`.
        // This tells it to parse the query portion of the URL.
        const parsedUrl = parse(url, true)

        handle(req, res, parsedUrl)
    }).listen(PORT, err => {
        if (err) throw err
        console.log(`> Ready on http://localhost:${PORT}`)
    })
})

https://nextjs.org/docs/advanced-features/custom-server ์ฐธ์กฐ

@mbrowne ์šฐ๋ฆฌ๋Š” ์‹ค์ œ๋กœ ์ปค์Šคํ…€ ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๋งŽ์€ ์ด์œ ๊ฐ€ ์žˆ์ง€๋งŒ ์ง€๊ธˆ๊นŒ์ง€ ๊ตฌํ˜„ํ•˜์ง€

ํ˜„์žฌ๋กœ์„œ๋Š” ์•ฑ์— ๋Œ€ํ•œ ์ž๋™ ์ •์  ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์‚ดํŽด๋ณด์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ ์‚ฌ์šฉ์ž ์ •์˜ ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์ง€๋งŒ ์ˆ˜์ •๋œ(์Šฌ๋ž˜์‹œ ์—†์ด) URL์„ handle ํ•˜๋ฉด SSR์€ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๋‹ค๋ฅธ URL์„ ๋ด…๋‹ˆ๋‹ค.
๋‚˜๋Š” ๊ทธ ๋ถˆ์พŒํ•œ ํ•ดํ‚น์—†์ด ์„ ํ–‰ ์Šฌ๋ž˜์‹œ๊ฐ€์žˆ๋Š” URL์„ ์ผ์น˜์‹œํ‚ค๊ธฐ ์œ„ํ•ด next ๋ผ์šฐํ„ฐ๋ฅผ ์„ ํ˜ธํ•ฉ๋‹ˆ๋‹ค.

2020๋…„์—๋„ ์ด ๋ฒ„๊ทธ๋Š” ์—ฌ์ „ํžˆ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋ฏฟ์„ ์ˆ˜ ์—†๋Š”

์ด๊ฒƒ์€ ์ •๋ง๋กœ ๊ณ ์ณ์•ผ ํ•  ๋‚˜์œ ๋ฒ„๊ทธ์ž…๋‹ˆ๋‹ค. /products ๋Š” ์ž‘๋™ํ•˜์ง€๋งŒ /products/ ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด ๋งํฌ๋กœ

<Link href="/products">
  <a>Products</a>
</Link>

๋‚˜๋Š” ์–ป๋‹ค

index.js:1 Warning: Prop `href` did not match. Server: "/products" Client: "/products/"

๊ทธ๋Ÿฌ๋‚˜ ๋งํฌ๋ฅผ /products/ ๊ฐ€๋ฆฌํ‚ค๊ณ  ๋งํฌ๋ฅผ ๋ฐฉ๋ฌธํ•˜๊ณ  ๊ฐœ๋ฐœ ์ค‘์— ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ ๊ณ ์น˜๋ฉด 404๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์ƒ๋‹นํžˆ ๊ณ ํ†ต์Šค๋Ÿฌ์šด ๊ฐœ๋ฐœ ๊ฒฝํ—˜์ž…๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋Š” 1.5๋…„ ์ „์— ์ฒ˜์Œ ๋ณด๊ณ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ๊ณต์‹์ ์ธ ์ˆ˜์ •์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? 9.3.4์—์„œ ์—ฌ์ „ํžˆ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

SEO๋ฅผ ์œ„ํ•ด ์ฝ˜ํ…์ธ ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋Œ€์‹  ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ URL๋กœ ๋ฆฌ๋””๋ ‰์…˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

app.prepare().then(() => {
  createServer((req, res) => {
    if (req.url !== '/' && req.url.endsWith('/')) {
      res.writeHead(301, { Location: req.url.slice(0, -1) })
      res.end()
    }
    handle(req, res, parse(req.url, true))
  }).listen(PORT, err => {
    if (err) throw err
    console.log(`> Ready on http://localhost:${PORT}`)
  })
})

SEO์˜ ๊ฒฝ์šฐ rel="canonical" ๊ฐ€ ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์ง€๋งŒ ์—ฌ์ „ํžˆ ์ด 404 ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ ์ •๋ง๋กœ ๊ณ ์ณ์•ผ ํ•  ๋‚˜์œ ๋ฒ„๊ทธ์ž…๋‹ˆ๋‹ค. /products ๋Š” ์ž‘๋™ํ•˜์ง€๋งŒ /products/ ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด ๋งํฌ๋กœ

<Link href="/products">
  <a>Products</a>
</Link>

๋‚˜๋Š” ์–ป๋‹ค

index.js:1 Warning: Prop `href` did not match. Server: "/products" Client: "/products/"

๊ทธ๋Ÿฌ๋‚˜ ๋งํฌ๋ฅผ /products/ ๊ฐ€๋ฆฌํ‚ค๊ณ  ๋งํฌ๋ฅผ ๋ฐฉ๋ฌธํ•˜๊ณ  ๊ฐœ๋ฐœ ์ค‘์— ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ ๊ณ ์น˜๋ฉด 404๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์ƒ๋‹นํžˆ ๊ณ ํ†ต์Šค๋Ÿฌ์šด ๊ฐœ๋ฐœ ๊ฒฝํ—˜์ž…๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋Š” 1.5๋…„ ์ „์— ์ฒ˜์Œ ๋ณด๊ณ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ๊ณต์‹์ ์ธ ์ˆ˜์ •์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? 9.3.4์—์„œ ์—ฌ์ „ํžˆ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๋‚˜๋Š” ๋˜ํ•œ ํ˜„์žฌ์ด ๋ฌธ์ œ๋ฅผ ๊ฒช๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆ˜์ • ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. https://medium.com/@thisisayush/handling -404-trailing-slash-error-in-nextjs-f8844545afe3

์ˆ˜์ • ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. https://medium.com/@thisisayush/handling -404-trailing-slash-error-in-nextjs-f8844545afe3

๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๋กœ์ปฌ์—์„œ ๊ฐœ๋ฐœํ•  ๋•Œ ์‚ฌ์šฉ์ž ์ง€์ • ์„œ๋ฒ„๊ฐ€ ํ•„์š”ํ•˜์ง€๋งŒ ํ•„์š”ํ•˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@timneutkens ์ด ๋ฌธ์ œ์— ๋Œ€ํ•œ ์ˆ˜์ • ์‚ฌํ•ญ์ด ๊ฐœ๋ฐœ ์ผ์ •์—

๋” ์ค‘์š”ํ•œ ๊ฒƒ์€ ๋ฆฌ๋””๋ ‰์…˜ ์†”๋ฃจ์…˜์ด ํ”„๋กœ๋•์…˜์—์„œ ์Šฌ๋ž˜์‹œ๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๋Œ€์‹  ์Šฌ๋ž˜์‹œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋„๋ก ์ด๋ฏธ ์„ค์ •๋œ ์‚ฌ์ดํŠธ๋ฅผ ์œ ์ง€ ๊ด€๋ฆฌํ•˜๋Š” ์‚ฌ๋žŒ๋“ค์—๊ฒŒ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ด ์„ ํƒ์„ ์ž„์˜๋กœ ์ง€์‹œํ•ด์„œ๋Š” ์•ˆ ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

@AlexSapoznikov ์˜ ์†”๋ฃจ์…˜ ์€ Netlify(๊ธฐ๋ณธ์ ์œผ๋กœ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์ถ”๊ฐ€ํ•จ)์™€ ํ•จ๊ป˜ ์ž˜ ์ž‘๋™ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜์— ๋Œ€ํ•œ ์ง€์›์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ณ ๊ธ‰ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค.

import App from "next/app";

export default class MyApp extends App {
  render() {
    const { Component, pageProps, router, router: { asPath } } = this.props;

    // Next.js currently does not allow trailing slash in a route, but Netlify appends trailing slashes. This is a
    // client side redirect in case trailing slash occurs. See https://github.com/zeit/next.js/issues/5214 for details
    if (asPath && asPath.length > 1) {
      const [path, query = ""] = asPath.split("?");
      if (path.endsWith("/")) {
        const asPathWithoutTrailingSlash = path.replace(/\/*$/gim, "") + (query ? `?${query}` : "");
        if (typeof window !== "undefined") {
          router.replace(asPathWithoutTrailingSlash, undefined, { shallow: true });
          return null;
        }
      }
    }

    return <Component {...pageProps} />;
  }
}

๋‹ค๋ฅธ SDK ๋ฐ ํ”Œ๋žซํผ์— ๋Œ€ํ•œ ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ๊ฒฝํ—˜์ด ์žˆ์ง€๋งŒ Next JS ์ดˆ๋ณด์ž์ด๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ๊ณผ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

์ด "๋ฒ„๊ทธ"๊ฐ€ ๊ฐ€์žฅ ๋†€๋ž๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๋‚˜์—๊ฒŒ ๊ทธ๊ฒƒ์€ "์ตœ์†Œ ๊ฒฝ์•…์˜ ์›์น™"์„ ์œ„๋ฐ˜ํ–ˆ์Šต๋‹ˆ๋‹ค. index.tsx๋ฅผ /page/about/ ํด๋”์— ๋„ฃ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— /about/ ๋ฐ /about์ด ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๋‚˜๋Š” 1990๋…„๋Œ€ ํ›„๋ฐ˜์— HTML FTP๋กœ ๋‚ด ์„œ๋ฒ„์— ์›น ์‚ฌ์ดํŠธ๋ฅผ ๋งŒ๋“ค๊ธฐ ์‹œ์ž‘ํ–ˆ๊ณ  ๋‚˜์ค‘์— PHP์™€ Apache, ๊ทธ๋ฆฌ๊ณ  ๊ฒฐ๊ตญ์—๋Š” Java ์„œ๋ฒ„๋กœ ์˜ฎ๊ฒผ์Šต๋‹ˆ๋‹ค. ์ง€๊ธˆ์€ ๋ชจ๋ฐ”์ผ ์•ฑ์„ ์ „๋ฌธ์œผ๋กœ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋™์ž‘์ด ๊ธฐ๋ณธ๊ฐ’์ด ์•„๋‹ˆ๋ฉฐ ๋‚ด dev ์„œ๋ฒ„์—์„œ ์ˆ˜์ •ํ•˜๋ ค๋ฉด ์‚ฌ์šฉ์ž ์ง€์ • ์„œ๋ฒ„ ํŽ˜์ด์ง€๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด ์ด์ƒํ•˜๊ฒŒ ๋Š๊ปด์ง‘๋‹ˆ๋‹ค.

์ •์  ๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ์ˆ˜ํ–‰ํ•  ๊ณ„ํš์ด๋ฏ€๋กœ ์‚ฌ์šฉ์ž ์ง€์ • ์„œ๋ฒ„๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ ํ”„๋กœ๋•์…˜์— ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ฐœ๋ฐœ ๋ฐ ๋””๋ฒ„๊น…์„ ์•ฝ๊ฐ„ ๋” ์„ฑ๊ฐ€์‹œ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

๊ฒŒ์œผ๋ฅธ ๊ฐœ๋ฐœ์ž๊ฐ€ ๊ฐœ๋ฐœ/๋””๋ฒ„๊ทธ ์‹œ๊ฐ„์„ ์œ„ํ•ด ์ถ”๊ฐ€ ๋ผ์šฐํŒ… ๋กœ์ง์„ ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†๋„๋ก ์ด ๋ฌธ์ œ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” "next dev" ํ”Œ๋ž˜๊ทธ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

๊ฐ์‚ฌ ํ•ด์š”!

ps: ์˜ˆ, /about ์™€ /about/ ๋Š” ์™„์ „ํžˆ ๋‹ค๋ฅธ URL์ด๋ผ๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. /pages/about/ ํด๋” ์•ˆ์— index.tsx ํŒŒ์ผ์„ ๋„ฃ์—ˆ์„ ๋•Œ ์ •๋ง ํ˜ผ๋ž€์Šค๋Ÿฌ์›Œ์„œ /about ๊ฒฝ๋กœ์—์„œ๋งŒ ์ž‘๋™ํ•˜์ง€๋งŒ /about/ ์—์„œ๋Š” ์ž‘๋™ /about/ . ๋ฐ˜๋Œ€์˜€๋‹ค๋ฉด ๋œ ๋†€๋ผ์ง€ ์•Š์•˜์„๊นŒ.

์กฐ๋‹ฌ์ฒญ : ๋‚˜๋Š”์ด ๋•Œ ์ถ”๊ฐ€ ํ˜ผ๋™ํ–ˆ๋‹ค <Link></Link> ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๊ทธ ํฌ์ธํŠธ /about/ ์˜ˆ์ƒ๋Œ€๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ƒˆ๋กœ ๊ณ ์นจ์„ ๋ˆ„๋ฅด๋ฉด URL์ด ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜๋”๋ผ๋„ ์ฆ‰์‹œ 404์ดˆ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์€ ๋งค์šฐ ๋†€๋ผ์šด ์ผ์ด์—ˆ์Šต๋‹ˆ๋‹ค. :-NS

ํ•˜์ง€๋งŒ ์ž ๊น, ์ƒํ™ฉ์ด ์•…ํ™”๋ฉ๋‹ˆ๋‹ค! ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๋ฆฌ๋””๋ ‰์…˜ํ•˜๋Š” ์‚ฌ์šฉ์ž ์ •์˜ checkForTrailingSlash ํ•จ์ˆ˜๋ฅผ _error.js ๋‚ด๋ถ€์— ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ (๋งˆ์นจ๋‚ด) ์‚ฌ์šฉ์ž ์ง€์ • 404 ํŽ˜์ด์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  Next.js๊ฐ€ ์‚ฌ์šฉ์ž ์ง€์ • 404 ํŽ˜์ด์ง€๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Error ์™„์ „ํžˆ ์šฐํšŒ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ•  ๋•Œ๊นŒ์ง€ ์ž ์‹œ ๋™์•ˆ Error.getInitialProps ๋‚ด๋ถ€์˜ ์‚ฌ์šฉ์ž ์ •์˜ ๋…ผ๋ฆฌ๊ฐ€ ๋” ์ด์ƒ ์ž‘๋™ํ•˜์ง€ ์•Š์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค(ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ ํ™•์ธ ํฌํ•จ).

์‚ฌ์šฉ์ž ์ •์˜ ์„œ๋ฒ„๋Š” ์•„์ง ๊ฐ€๋Šฅ์„ฑ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์ด ์–ธ๊ธ‰ํ•œ _app.js ์†”๋ฃจ์…˜์„ ์‹œ๋„ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

@AlexSapoznikov ์˜ ์†”๋ฃจ์…˜ ์€ Netlify(๊ธฐ๋ณธ์ ์œผ๋กœ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์ถ”๊ฐ€ํ•จ)์™€ ํ•จ๊ป˜ ์ž˜ ์ž‘๋™ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜์— ๋Œ€ํ•œ ์ง€์›์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ณ ๊ธ‰ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค.

import App from "next/app";

export default class MyApp extends App {
  render() {
    const { Component, pageProps, router, router: { asPath } } = this.props;

    // Next.js currently does not allow trailing slash in a route, but Netlify appends trailing slashes. This is a
    // client side redirect in case trailing slash occurs. See https://github.com/zeit/next.js/issues/5214 for details
    if (asPath && asPath.length > 1) {
      const [path, query = ""] = asPath.split("?");
      if (path.endsWith("/")) {
        const asPathWithoutTrailingSlash = path.replace(/\/*$/gim, "") + (query ? `?${query}` : "");
        if (typeof window !== "undefined") {
          router.replace(asPathWithoutTrailingSlash, undefined, { shallow: true });
          return null;
        }
      }
    }

    return <Component {...pageProps} />;
  }
}

์ฝ”๋“œ ์ƒ˜ํ”Œ์— ์น˜๋ช…์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ์žˆ๋Š” ์ธ๋ฑ์Šค ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ์š”์ฒญ์€ ์˜ค๋ฅ˜๋ฅผ ๋˜์ง‘๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ ์ฟผ๋ฆฌ ๋ฌธ์ž์—ด๋งŒ asPath ๋กœ Next.js์— ์ „๋‹ฌํ•˜๋ ค๊ณ  ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค.

  if (asPath && asPath.length > 1) {
    const [path, query = ''] = asPath.split('?');
    if (path.endsWith('/') && path.length > 1) {
      const asPathWithoutTrailingSlash =
        path.replace(/\/*$/gim, '') + (query ? `?${query}` : '');
      if (typeof window !== 'undefined') {
        router.replace(asPathWithoutTrailingSlash, undefined, {
          shallow: true,
        });
        return null;
      }
    }
  }

SSR์—์„œ ์ด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋ ค๋ฉด @pjaws & @AlexSapoznikov ์†”๋ฃจ์…˜์— ๋‹ค์Œ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  static async getInitialProps({ Component, ctx, router }) {
    /* Fixes the trailing-slash-404 bug for server-side rendering. */
    const { asPath } = router;
    if (asPath && asPath.length > 1) {
      const [path, query = ""] = asPath.split("?");
      if (path.endsWith("/") && path.length > 1) {
        const asPathWithoutTrailingSlash =
          path.replace(/\/*$/gim, "") + (query ? `?${query}` : "");
        if (ctx.res) {
          ctx.res.writeHead(301, {
            Location: asPathWithoutTrailingSlash,
          });
          ctx.res.end();
        }
      }
    }
    return {
      pageProps: Component.getInitialProps
        ? await Component.getInitialProps(ctx)
        : {},
    };
  }

์•„๋งˆ๋„ ์ด ๊ธฐ๋Šฅ์„ SSR ๋™์•ˆ๊ณผ CSR ๋™์•ˆ ๋ชจ๋‘ ์ž‘๋™ํ•˜๋Š” ํ•จ์ˆ˜๋กœ ์ผ๋ฐ˜ํ™”ํ•˜๊ณ  ๋‘ ์œ„์น˜( getInitialProps ๋ฐ render )์—์„œ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

~์— ์˜ํ•ด

์ด๊ฒƒ์€ ์ˆ˜์ •๋˜์ง€๋งŒ ์ œ๋ชฉ์ด ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ 
image

@AlexSapoznikov @pjaws

๊ท€ํ•˜์˜ ์†”๋ฃจ์…˜์€ ์šฐ๋ฆฌ๋ฅผ ๋ฌดํ•œ ๋ฃจํ”„์— ๋น ๋œจ๋ฆฝ๋‹ˆ๋‹ค.

  if (asPath && asPath.length > 1) {
    const [path, query = ''] = asPath.split('?');
    if (path.endsWith('/') && path.length > 1) {
      const asPathWithoutTrailingSlash =
        path.replace(/\/*$/gim, '') + (query ? `?${query}` : '');
      if (typeof window !== 'undefined') {
        router.replace(asPathWithoutTrailingSlash, undefined, {
          shallow: true,
        });
        return null;
      }
    }
  }

๋ฌธ๋งฅ

ํ†ต์ œํ•  ์ˆ˜ ์—†๋Š” ์ด์œ ๋กœ ์ธํ•ด next.config.js ์—์„œ exportTrailingSlash ์˜ต์…˜์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค๋ฅธ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ๋งํฌ๋ฅผ ๊ฐ–๊ณ  ์‹ถ์ง€๋งŒ ๋งํฌ๊ฐ€ /somepage?param=whatever ๊ฐ€ ๋˜๊ธฐ๋ฅผ ์›ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ๋งํฌ๋Š” ์ด๊ฒƒ์„ /somepage/?param=whatever ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

์œ„์˜ ์†”๋ฃจ์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด params ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜์ง€๋งŒ /somepage/ ์™€ ๊ฐ™์€ ๋ฐฐํฌ๋œ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋ฉด ๋ฌดํ•œ ๋ฃจํ”„์— ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค.

@ronyeh ๊ฐ€ ์—ฌ๊ธฐ์—์„œ ์ •๋ง ์ข‹์€ ์ง€์ ์„ ํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ €๋Š” ์ด ๋ฌธ์ œ์— ๋Œ€ํ•œ ๊ณต์‹์ ์ธ ํ•ด๊ฒฐ์ฑ…์„ ์ •๋ง๋กœ ์›ํ•ฉ๋‹ˆ๋‹ค :(

SSR์—์„œ ์ด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋ ค๋ฉด @pjaws & @AlexSapoznikov ์†”๋ฃจ์…˜์— ๋‹ค์Œ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  static async getInitialProps({ Component, ctx, router }) {
    /* Fixes the trailing-slash-404 bug for server-side rendering. */
    const { asPath } = router;
    if (asPath && asPath.length > 1) {
      const [path, query = ""] = asPath.split("?");
      if (path.endsWith("/") && path.length > 1) {
        const asPathWithoutTrailingSlash =
          path.replace(/\/*$/gim, "") + (query ? `?${query}` : "");
        if (ctx.res) {
          ctx.res.writeHead(301, {
            Location: asPathWithoutTrailingSlash,
          });
          ctx.res.end();
        }
      }
    }
    return {
      pageProps: Component.getInitialProps
        ? await Component.getInitialProps(ctx)
        : {},
    };
  }

์•„๋งˆ๋„ ์ด ๊ธฐ๋Šฅ์„ SSR ๋™์•ˆ๊ณผ CSR ๋™์•ˆ ๋ชจ๋‘ ์ž‘๋™ํ•˜๋Š” ํ•จ์ˆ˜๋กœ ์ผ๋ฐ˜ํ™”ํ•˜๊ณ  ๋‘ ์œ„์น˜( getInitialProps ๋ฐ render )์—์„œ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ getServerSideProps๊ฐ€ ์žˆ๋Š” ํŽ˜์ด์ง€์—์„œ ์ž‘๋™ํ–ˆ์œผ๋ฉฐ ์ด์ œ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๊ฐ€ ์žˆ๋Š” URL์ด 404 ์—†์ด ๋™์ผํ•œ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
๊ทธ๋Ÿฌ๋‚˜ ํ•œ ๊ฐ€์ง€ ๊ฒฐํ•จ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์  ๊ฒฝ๋กœ์™€ getStaticPaths๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํŽ˜์ด์ง€๊ฐ€ ๊ฑฐ์˜ ์—†์Šต๋‹ˆ๋‹ค. getServerSideProps๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ์ด๋Ÿฌํ•œ ๋™์  ๊ฒฝ๋กœ๋ฅผ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋กœ ํƒ์ƒ‰ํ•˜๋ฉด ๋จผ์ € 404๋ฅผ ๋ฐ˜ํ™˜ํ•œ ๋‹ค์Œ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜๋ฉ๋‹ˆ๋‹ค. .

/api/test ํด๋”๋กœ ์ž‘์—… ์ค‘์ž…๋‹ˆ๋‹ค.

  • ํŽ˜์ด์ง€/api/test.tsx
  • ํŽ˜์ด์ง€/API/ํ…Œ์ŠคํŠธ/[ID].tsx

๊ทธ๊ฒƒ์€ ์ž‘๋™

  • GET /API/ํ…Œ์ŠคํŠธ
  • GET /api/test/123
  • GET /api/test/123/

๊ทธ๋ฆฌ๊ณ  ๋‚˜๋Š” ์ด๊ฒƒ์ด ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.

  • GET /api/test/

์ด๊ฒƒ์ด ๊ด€๋ จ ๋ฌธ์ œ์ธ์ง€ ํ™•์‹คํ•˜์ง€ ์•Š์Œ
P/D exportTrailingSlash = true๋กœ ํ•ด๊ฒฐ๋˜์ง€ ์•Š์Œ

์ด๊ฒƒ์€ ๋งค์šฐ ์˜ค๋ž˜๋œ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ฒŒ ์˜ค๋žซ๋™์•ˆ ํ•ด๊ฒฐ๋˜์ง€ ์•Š๋Š” ์ด์œ ๊ฐ€ ์žˆ์Šต๋‹ˆ๊นŒ?

๋ฌด์—‡์ด ๋” ์ด์ƒ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”์ง€ ์ž˜ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค.

๋‚ด ๋ง์€ ์š”๊ตฌ ์‚ฌํ•ญ์ด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

| | exportTrailingSlash: ๊ฑฐ์ง“ | exportTrailingSlash: ์ฐธ |
|-------------------------|-------------------------- -----|------------------------------|
| url์€ / | ์ž‘๋™ํ•˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค | ์ž‘๋™ํ•ด์•ผ |
| url์ด / |๋กœ ๋๋‚˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ž‘๋™ํ•ด์•ผ | ์ž‘๋™ํ•˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค |

๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ ์˜ˆ์ƒ๋Œ€๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

  • ํ˜„์ง€์—์„œ๋Š” exportTrailingSlash: false
  • ๋ฐฐํฌ(ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ)์˜ ๊ฒฝ์šฐ exportTrailingSlash: true ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  nginx๋Š” url/ ๋ฅผ url/index.html

@andrescabana86 ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋Š” GET /api/test/123/ ๋ฐ˜๋ฉด GET /api/test/ ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š๊ณ  ์ž‘๋™ํ•˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@Izhaki ๋‚˜๋Š” prod์— ๋ฐฐํฌํ•˜์—ฌ ๋‘˜ ๋‹ค ์‹œ๋„ํ–ˆ์ง€๋งŒ ... ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  • GET /api/test/

exportTrailingSlash: true

์›ํ•˜๋Š” ๊ฒฝ์šฐ ๊ณต๊ฐœ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ค‘๊ฐ„์— ๋ญ”๊ฐ€๋ฅผ ์žŠ์–ด๋ฒ„๋ ธ์„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ต๋ณ€ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค

@andrescabana86 ๊ณต๊ฐœ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ฐ€ ์—ฌ๊ธฐ์—์„œ ์–ผ๋งˆ๋‚˜ ๋„์›€์ด ๋ ์ง€ ์ž˜ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ฐฐํฌํ•˜๋Š” ์„œ๋ฒ„์˜ ์ผ๋ถ€ ๊ตฌ์„ฑ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

package.json ์—์„œ ์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ์ปฌ์—์„œ ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ( exportTrailingSlash: true )๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

"serve:out": "docker run --rm -v $(pwd)/out:/static -p 5000:80 flashspys/nginx-static"

๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:5000/api/test/ ์ž‘๋™ํ•˜๋Š”์ง€ ์•Œ๋ ค์ฃผ์‹ญ์‹œ์˜ค.

(์ฃผ๋Š” ๊ฒƒ์„ $(pwd) ๋งฅ / ๋ฆฌ๋ˆ…์Šค์— - ๋ณผ ์ด ์ฐฝ๋ฌธ์„)

@Izhaki ๋ฌธ์ œ๋Š” (์ดˆ๊ธฐ ๋ณด๊ณ ์„œ์—์„œ ์•Œ ์ˆ˜ ์žˆ๋“ฏ์ด) "์ ๋ฒ•ํ•œ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ๋งํฌ์˜ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก ํƒ์ƒ‰์—๋Š” ์ž‘๋™ํ•˜์ง€๋งŒ ํ•˜๋“œ ์ƒˆ๋กœ ๊ณ ์นจ(ssr) ์‹œ ๋ฒˆ๋“ค ๋ฐ 404๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ์œผ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค"๋ผ๋Š” ์‚ฌ์‹ค์— ๊ด€ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํด๋ผ์ด์–ธํŠธ ์ธก ๊ฒฝ๋กœ ๋ณ€๊ฒฝ ๋™์ž‘๊ณผ ํ•˜๋“œ ์ƒˆ๋กœ ๊ณ ์นจ ์‚ฌ์ด์— ๋ถˆ์ผ์น˜๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ตœ์‹  ๋ฒ„์ „์˜ Next.js์—์„œ๋„ ๋ฌธ์ œ๊ฐ€ ์ง€์†๋˜๋Š”์ง€ ์ž˜ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋ฅผ ๋งˆ์น˜๋ฉด ์—ฌ๊ธฐ์— ๋‹ค์‹œ ๋ณด๊ณ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

9.4.1 ๋ฐ exportTrailingSlash: true ํ…Œ์ŠคํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

http://localhost:6500/admin/ ๋กœ ์ด๋™ํ•˜๋ฉด ๋กœ์ปฌ์—์„œ ๊ฐœ๋ฐœํ•  ๋•Œ 404๊ฐ€ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋‚ด๋ณด๋‚ผ ๋•Œ๋„ ๋™์ผํ•œ ๊ฒฝ๋กœ๊ฐ€ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

exportTrailingSlash ๋Š” ์ด๊ฒƒ์ด _exports_ ์ „์šฉ์ž„์„ ์•”์‹œํ•ฉ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ํ•˜๋Š” ์ผ์€ ๋‹ค์Œ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

exportTrailingSlash: process.env.NODE_ENV === 'production'

์ฆ‰, ๋กœ์ปฌ์—์„œ ๊ฐœ๋ฐœํ•  ๋•Œ ์ผ์ด ์˜๋„ํ•œ ๋Œ€๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ฐฐํฌ ์‹œ(๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ํ†ตํ•ด) ์ œ๋Œ€๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

์ด๊ฒƒ์— ๋Œ€ํ•œ ์ •ํ™•ํ•˜๊ณ  ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์†”๋ฃจ์…˜์ด ์•„๋‹Œ๊ฐ€์š”?

URL์ด ๊ฐœ๋ฐœ์—์„œ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š์ง€๋งŒ ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ž‘๋™ํ•œ๋‹ค๋ฉด ์ตœ์†Œ ๋†€๋ผ์›€์˜ ์›์น™์— ์–ด๊ธ‹๋‚œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์ง€ ์•Š์Šต๋‹ˆ๊นŒ? ๋‚˜๋Š” ์ด๊ฒƒ์ด ์—ฌ์ „ํžˆ ๋ฒ„๊ทธ๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

^ ์ฆ‰, ์ด์ „์— ํ”„๋กœ๋•์…˜ ๋‹จ๊ณ„์—์„œ ํŽ˜์ด์ง€ ์ƒˆ๋กœ ๊ณ ์นจ๊ณผ router.push ์ด๋ฒคํŠธ ๊ฐ„์— ์ถฉ๋Œ ๋™์ž‘์ด ์žˆ์—ˆ๋‹ค๊ณ  ํ™•์‹ ํ•ฉ๋‹ˆ๋‹ค. ์ง€๊ธˆ๋„ ๊ทธ๋Ÿฌ๋Š”์ง€ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค.

@andrescabana86 @Izhaki exportTrailingSlash ๋Š” ์ด๊ฒƒ๊ณผ ๊ด€๋ จ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ด ์˜ต์…˜์€ Next.js ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ •์  ๋‚ด๋ณด๋‚ด๊ธฐ์™€ ๊ด€๋ จ์ด ์žˆ์Šต๋‹ˆ๋‹ค. true์ด๋ฉด example/index.html ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  false์ด๋ฉด example.html ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ๋‚ด ์ดํ•ด๋Š” exportTrailingSlash ๊ฐ€ ๊ฐœ๋ฐœ ๋ชจ๋“œ์™€ ๊ด€๋ จ์ด ์—†๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ˜ผ๋ž€์˜ ์›์ธ ์ค‘ ํ•˜๋‚˜๋Š” exportTrailingSlash ๊ฐ€ ์žˆ์„ ๋•Œ next.js๊ฐ€ ๋งํฌ์— ์Šฌ๋ž˜์‹œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ๊ฐœ๋ฐœ์—์„œ๋„ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•ด์•ผ ํ•˜๋Š”์ง€ ์ž˜ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์–ด์จŒ๋“  ์ด๊ฒƒ์€ example/index.html ๋Œ€ example.html ์— ๊ด€ํ•œ ๋ฌธ์ œ์ผ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ˆ˜์ •ํ•ด์•ผ ํ•˜๋Š” ๋งํฌ๋„ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

URL์ด ๊ฐœ๋ฐœ์—์„œ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š์ง€๋งŒ ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ž‘๋™ํ•œ๋‹ค๋ฉด ์ตœ์†Œ ๋†€๋ผ์›€์˜ ์›์น™์— ์–ด๊ธ‹๋‚œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์ง€ ์•Š์Šต๋‹ˆ๊นŒ? ๋‚˜๋Š” ์ด๊ฒƒ์ด ์—ฌ์ „ํžˆ ๋ฒ„๊ทธ๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๊ฐ€ ํ‹€๋ฆด ์ˆ˜๋„ ์žˆ์ง€๋งŒ exportTrailingSlash ์˜ต์…˜์€ URL์ด /something ๋•Œ /something.html ๋ฅผ ์ œ๊ณตํ•˜๋„๋ก ๊ตฌ์„ฑ๋˜์ง€ ์•Š์€ nginx ์„œ๋ฒ„์šฉ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ ๋กœ์ปฌ ๊ฐœ๋ฐœ์— ์‚ฌ์šฉ๋˜๋Š” ๋‹ค์Œ ์„œ๋ฒ„์˜ ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ๊ณผ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์€ ์•ฑ์„ ์ œ๊ณตํ•˜๋Š” ํ•ญ๋ชฉ์— ๋”ฐ๋ผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

๋‹น์‹ ์€ ๋•Œ ๊ฒฝ์šฐ ์ˆ˜ exportTrailingSlash (์ด ๊ฒƒ ์žˆ์ง€๋งŒ ์‚ฌ์‹ค, ๋‹ค์Œ ์„œ๋ฒ„๊ฐ€ ์Šฌ๋ž˜์‹œ๋กœ ๋๋‚˜์•ผ ๊ฒฝ๋กœ๋ฅผ ์ง€์›ํ•ด์•ผ์„ export ์—์„œ exportTrailingSlash ๋‹ค์†Œ ๋ฌด๊ด€).

FWIW ์ด๊ฒƒ์€ ์ด๋ฏธ #13333์—์„œ ์ž‘์—… ์ค‘์ž…๋‹ˆ๋‹ค.

์ €๋Š” ๊ฒฝํ—˜์ด ๋งŽ์ง€ ์•Š์€ ์ฝ”๋”๋กœ ์ฃผ๋กœ ๋‹ค์ค‘ ํŽ˜์ด์ง€ ๋žœ๋”ฉ์— Next.js๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ถ„๋ช…ํžˆ ๋‚˜๋Š” โ€‹โ€‹๊ทธ ํšจ๊ณผ๋ฅผ ์•Œ์ง€ ๋ชปํ•˜๋Š” ์‚ฌ์ด์— ๋‹ค์Œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ๊ฑฐ์˜ ํ•ญ์ƒ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ์ œ๊ฑฐ๋œ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค.

// In your server.js
server.get('/:id', (req, res) => {
  const actualPage = `/${req.params.id}`
  app.render(req, res, actualPage)
})

์ œ ๊ฒฝ์šฐ์—๋Š” ์ถ”๊ฐ€ ์ •์  URL ์ ‘๋‘์‚ฌ ๋“ฑ์„ ์ง€์›ํ•˜๊ธฐ ์œ„ํ•ด ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ๊ฐ€ ์กฐ๊ธˆ ๋” ๋ณต์žกํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด ์ œ๊ฑฐ๋œ ๋ฒ„์ „์€ exportTrailingSlash ๊ด€๊ณ„์—†์ด ๋…ผ์˜๋œ ๋ฌธ์ œ์— ๋Œ€ํ•ด ์ž˜ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. Link ์— ๋Œ€ํ•œ ์„ค์ • ๋ฐ ์˜ํ–ฅ. ์˜ˆ๋ฅผ ๋“ค์–ด URL /about ๋ฐ /about/ ๋Š” ์ž˜ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ํ˜•ํƒœ์—์„œ๋Š” ๋ณธ์งˆ์ ์œผ๋กœ Next.js์˜ ๊ธฐ๋ณธ ๋ผ์šฐํŒ…์„ ๋ชจ๋ฐฉํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ : ์‚ฌ์šฉ์ž ์ •์˜ server.js ๊ฐ€ ํ•„์š”ํ•˜๋ฉฐ "๋” ๊นŠ์€" URL(์ถ”๊ฐ€ "ํ•˜์œ„ ํด๋”" ํฌํ•จ)์— ๋Œ€ํ•ด ์ˆ˜๋™์œผ๋กœ ์ง€์›ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค(์˜ˆ: /company/about/ . ๊ทธ๋Ÿฌ๋‚˜ ์ด๋ฏธ ํ”„๋กœ์ ํŠธ์—์„œ server.js ์‚ฌ์šฉ์ž ์ •์˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ๋žŒ๋“ค์—๊ฒŒ๋Š” ๋น„๊ต์  ๊ฐ„๋‹จํ•œ ์†”๋ฃจ์…˜์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

SSR์—์„œ ์ด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋ ค๋ฉด @pjaws & @AlexSapoznikov ์†”๋ฃจ์…˜์— ๋‹ค์Œ์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  static async getInitialProps({ Component, ctx, router }) {
    /* Fixes the trailing-slash-404 bug for server-side rendering. */
    const { asPath } = router;
    if (asPath && asPath.length > 1) {
      const [path, query = ""] = asPath.split("?");
      if (path.endsWith("/") && path.length > 1) {
        const asPathWithoutTrailingSlash =
          path.replace(/\/*$/gim, "") + (query ? `?${query}` : "");
        if (ctx.res) {
          ctx.res.writeHead(301, {
            Location: asPathWithoutTrailingSlash,
          });
          ctx.res.end();
        }
      }
    }
    return {
      pageProps: Component.getInitialProps
        ? await Component.getInitialProps(ctx)
        : {},
    };
  }

์•„๋งˆ๋„ ์ด ๊ธฐ๋Šฅ์„ SSR ๋™์•ˆ๊ณผ CSR ๋™์•ˆ ๋ชจ๋‘ ์ž‘๋™ํ•˜๋Š” ํ•จ์ˆ˜๋กœ ์ผ๋ฐ˜ํ™”ํ•˜๊ณ  ๋‘ ์œ„์น˜( getInitialProps ๋ฐ render )์—์„œ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์ด๊ฒƒ์€ getServerSideProps๊ฐ€ ์žˆ๋Š” ํŽ˜์ด์ง€์—์„œ ์ž‘๋™ํ–ˆ์œผ๋ฉฐ ์ด์ œ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๊ฐ€ ์žˆ๋Š” URL์ด 404 ์—†์ด ๋™์ผํ•œ ํŽ˜์ด์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
๊ทธ๋Ÿฌ๋‚˜ ํ•œ ๊ฐ€์ง€ ๊ฒฐํ•จ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์  ๊ฒฝ๋กœ์™€ getStaticPaths๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํŽ˜์ด์ง€๊ฐ€ ๊ฑฐ์˜ ์—†์Šต๋‹ˆ๋‹ค. getServerSideProps๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ์ด๋Ÿฌํ•œ ๋™์  ๊ฒฝ๋กœ๋ฅผ ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋กœ ํƒ์ƒ‰ํ•˜๋ฉด ๋จผ์ € 404๋ฅผ ๋ฐ˜ํ™˜ํ•œ ๋‹ค์Œ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜๋ฉ๋‹ˆ๋‹ค. .

@gauravkrp @AlexSapoznikov ์†”๋ฃจ์…˜์€ ์‹ค์ œ๋กœ ์—ฌ์ „ํžˆ ํŽ˜์ด์ง€์— ๋Œ€ํ•œ 404๋ฅผ Google์— ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฒƒ์€ ์‹ค์ œ๋กœ ๋งค์šฐ ์ค‘์š”ํ•œ ์ถ”๊ฐ€ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค(๋ฆฌ๋””๋ ‰์…˜์ด ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์—). SEO๊ฐ€ ์šฐ๋ฆฌ ์ค‘ ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์ด ์ฒ˜์Œ์— Next.js๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ฃผ์š” ์ด์œ ๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๋‚˜๋Š” ๋˜ํ•œ ์ด๊ฒƒ์„ getInitialProps ์— ๋„ฃ๋Š” ๊ฒƒ์ด ๋ชจ๋“  ๋ฉด์—์„œ ์ž‘๋™ํ•ด์•ผ ํ•˜๊ณ , ์ด ์‹œ์ ์—์„œ main ํ•จ์ˆ˜ ๋‚ด๋ถ€์˜ ๋ถ€๋ถ„์€ ๋ถˆํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์ฃผ์˜ํ•  ์ ์€ ์ž๋™ ์ •์  ์ตœ์ ํ™”๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™ ์ •์  ์ตœ์ ํ™”๋ฅผ ์žƒ๊ฒŒ ๋œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ 404๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค๋Š” ๋‚˜์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ณต์œ ๋ฅผ ์œ„ํ•ด...

๋‚ด ํ”„๋กœ์ ํŠธ๋Š” Express + Next.js ์ž…๋‹ˆ๋‹ค.
express 4.17.1
next 9.4.5-canary.7

๊ฐœ๋ฐœ์‹œ

๋™์  ๋Ÿฐํƒ€์ž„

// next.config.js
module.exports = {
  exportTrailingSlash: false,
};

// app.js
const Next = require('next').default;
const NextApp = Next({ dev });
const NextHandler = NextApp.getRequestHandler();
NextApp.prepare();
app.get('*', (req, res) => NextHandler(req, res));

์ƒ์‚ฐ์‹œ

์ •์  ๋‚ด๋ณด๋‚ด๊ธฐ
next build ๋ฐ next export -o dist/

// next.config.js
module.exports = {
  exportTrailingSlash: true,
};

// app.js
app.use('/_next', express.static('dist/_next', { etag: true, index: false, maxAge: '365d', redirect: false, dotfiles: 'ignore' }));
app.use('/fonts', express.static('dist/fonts', { etag: true, index: false, maxAge: '365d', redirect: false, dotfiles: 'ignore' }));
app.use('/img', express.static('dist/img', { etag: true, index: false, maxAge: '365d', redirect: false, dotfiles: 'ignore' }));
app.use(express.static('./dist', { index: ['index.html'] }));
app.use((req, res) => {
  res.Redirect('/404'); // <- Express will auto handle both /404 or /404/
});

๊ฒฐ๋ก ์ ์œผ๋กœ

ํด๋ผ์ด์–ธํŠธ ์•ฑ์„ ํด๋ฆญํ•˜์—ฌ ๋ฆฌ๋””๋ ‰์…˜ํ•  ๋•Œ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
๋˜ํ•œ ํ•˜๋“œ ์ƒˆ๋กœ ๊ณ ์นจ์ด static route ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ dynamic route ์—์„œ ํ•˜๋“œ ์ƒˆ๋กœ ๊ณ ์นจ์„ ํ•˜๋ฉด 404๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.
/album/[id].jsx ๋˜๋Š” /album/123 ,
๋”ฐ๋ผ์„œ ๋‹ค์Œ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ
/album/123 ์—์„œ 404์— ๋„๋‹ฌํ•˜๋ฉด
์„œ๋ฒ„๋Š” html ์ฝ˜ํ…์ธ ๋ฅผ ๊ณ„์† ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
๋ธŒ๋ผ์šฐ์ €๋Š” ๋ฌธ์ œ ์—†์ด ํŽ˜์ด์ง€๋ฅผ ๊ณ„์† ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.
Next.js๊ฐ€ ๋ถ€ํŒ…๋˜๋ฉด next/router ๊ฐ€ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ”„๋กœ๋•์…˜์—์„œ ์ด ๋ฌธ์ œ์— ๋Œ€ํ•œ ์ž„์‹œ ํ•ด๊ฒฐ์ฑ…์ด ์žˆ์Šต๋‹ˆ๊นŒ?

์šฐ๋ฆฌ๋Š” ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ณง ์ถœ์‹œํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

ํ”„๋กœ๋•์…˜์—์„œ ์ด ๋ฌธ์ œ์— ๋Œ€ํ•œ ์ž„์‹œ ํ•ด๊ฒฐ์ฑ…์ด ์žˆ์Šต๋‹ˆ๊นŒ?

์ด ์Šค๋ ˆ๋“œ์—๋Š” ๋งŽ์€ ๊ฒƒ์ด ์žˆ์ง€๋งŒ ํ˜„์žฌ @gauravkrp ๊ฐ€ ์ตœ๊ทผ์— ๊ฒŒ์‹œํ•œ ๊ฒƒ์„ ์‚ฌ์šฉํ•˜๊ณ 

์—ฌ๊ธฐ์—์„œ PR์„ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. #13333

์ด๊ฒƒ์€ ์ด์ œ next@^9.4.5-canary.17 ์—์„œ ํ•ด๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

๊ธฐ๋Šฅ์ด ์นด๋‚˜๋ฆฌ์•„์—์„œ ๋งˆ์Šคํ„ฐ๋กœ ์ด๋™ํ•˜๋Š” ๋ฐ ์–ผ๋งˆ๋‚˜ ๊ฑธ๋ฆฝ๋‹ˆ๊นŒ?

์ด๊ฒƒ์€ ์ด์ œ next@^9.4.5-canary.17 ์—์„œ ํ•ด๊ฒฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

๊ทธ๋ฆฌ๊ณ  ์ •ํ™•ํžˆ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐ๋˜๋‚˜์š”? ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๊นŒ? ๋‚ด๊ฐ€ ์•ก์„ธ์Šค "๊ฒฝ์šฐ www.site.com/help/ "๋‚ด๊ฐ€๋กœ ๋ฆฌ๋””๋ ‰์…˜ " www.site.com/help "์šฐ๋ฆฌ๋Š” ์šฐ๋ฆฌ๊ฐ€ ์Šฌ๋ž˜์‹œ๋ฅผ ์ข…๋ฃŒ ๋– ๋‚˜์ด ์„ ํƒํ•˜๋Š” ์˜ต์…˜์„ ๊ฐ€์งˆ ์ˆ˜์žˆ๋‹ค? " www.site.com/help/ " ๋˜๋Š” " www.site.com/help "์— ์•ก์„ธ์Šคํ•˜๋ฉด ์ข…๋ฃŒ๋˜๊ฑฐ๋‚˜ ๋ฆฌ๋””๋ ‰์…˜๋˜๊ฑฐ๋‚˜ ๋์— "/"๊ฐ€ ์ถ”๊ฐ€๋˜์–ด " www.site.com/help/ "๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

@Valnexus ๋Š” #

module.exports = {
  experimental: {
    trailingSlash: true
  }
}

๊ธฐ๋Šฅ์ด ์นด๋‚˜๋ฆฌ์•„์—์„œ ๋งˆ์Šคํ„ฐ๋กœ ์ด๋™ํ•˜๋Š” ๋ฐ ์–ผ๋งˆ๋‚˜ ๊ฑธ๋ฆฝ๋‹ˆ๊นŒ?

์ค€๋น„๊ฐ€ ๋˜๋ฉด ์ฒ˜๋ฆฌ ์ค‘์ธ ์ฒ˜๋ฆฌ์— ์—ฌ์ „ํžˆ ์—ฃ์ง€ ์ผ€์ด์Šค๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋‹จ ๊ทธ๊ฒƒ๋“ค์ด ์ˆ˜์ •๋˜๋ฉด ์•ˆ์ •์ ์œผ๋กœ ๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@imneutkens @Janpot

์ตœ์‹  ๋‹ค์Œ ์นด๋‚˜๋ฆฌ์•„(9.4.5-canary.27)๋ฅผ ์‹œ๋„ํ–ˆ์ง€๋งŒ test ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค๊ณ  www.example/test/ ์•ก์„ธ์Šคํ•˜๋ฉด www.example/test ๋ฆฌ๋””๋ ‰์…˜๋ฉ๋‹ˆ๋‹ค.
๋‚˜๋Š” ๋‘ ๊ฒฝ์šฐ์˜ ํ–‰๋™์ด ๊ฐ™์•„์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๋•Œ ์•ก์„ธ์Šค www.example/test/ ๊ฐ€์— ๋จธ๋ฌผํ•ด์•ผ www.example/test/ .
๋•Œ ์•ก์„ธ์Šค www.example/test ๊ฐ€์— ๋จธ๋ฌผํ•ด์•ผ www.example/test .
Nuxt.js์—์„œ ํ…Œ์ŠคํŠธํ–ˆ๋Š”๋ฐ ์œ„์—์„œ ์„ค๋ช…ํ•œ ๊ฒƒ๊ณผ ๋™์ผํ•œ ๋™์ž‘์„ ํ•ฉ๋‹ˆ๋‹ค.

๋‚˜๋Š” ๋‘ ๊ฒฝ์šฐ์˜ ํ–‰๋™์ด ๊ฐ™์•„์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๋ฆฌ๋””๋ ‰์…˜์˜ ์ด์œ ๋Š” ๊ฒ€์ƒ‰ ์—”์ง„์— ์ค‘๋ณต ์ฝ˜ํ…์ธ ๊ฐ€ ํ‘œ์‹œ๋˜์ง€ ์•Š๋„๋ก ํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค. ์ •ํ™•ํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

์•„์ง ์•ˆ์ •์ ์ธ ๋ฆด๋ฆฌ์Šค๋กœ ๋ณ‘ํ•ฉ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ํ์‡„๋œ ๋ฌธ์ œ์ธ ์ด์œ ๋ฅผ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋‚ด๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ดํ•ดํ–ˆ๋‹ค๋ฉด ์ง€๊ธˆ์€ ์นด๋‚˜๋ฆฌ์•„ ๋ฆด๋ฆฌ์Šค์—์„œ๋งŒ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋งž์Šต๋‹ˆ๊นŒ?

๋ฌธ์ œ๋Š” ์นด๋‚˜๋ฆฌ์•„์—์„œ ์ฆ‰์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๊ด€๋ จ pull ์š”์ฒญ์ด ๋„์ฐฉํ•˜๋ฉด ๋‹ซํž™๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์นด๋‚˜๋ฆฌ์•„ ์ฑ„๋„๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜์‹ญ์‹œ์˜ค.

์ž˜ ๋“ค๋ฆฐ๋‹ค. @Timer๋‹˜, ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

@Janpot https://github.com/issues/ ๋ฐ https://github.com/issues ๋ฆฌ๋””๋ ‰์…˜ ์—†์ด ๋™์ผํ•œ ๋™์ž‘์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

https://twitter.com/explore/ ๋ฐ https://twitter.com/explore , ์ด๊ฒƒ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.

๊ฒ€์ƒ‰ ์—”์ง„์— ๋ฌธ์ œ๊ฐ€ ์žˆ์œผ๋ฉด Github์™€ Twitter์—์„œ ํ•ด๊ฒฐํ•˜์ง€ ์•Š์€ ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?
๋‚˜๋Š” ๊ทธ๊ฒƒ์ด ๋ชจ๋“  ์›น ์‚ฌ์ดํŠธ์˜ ๊ธฐ๋ณธ ๋™์ž‘์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

ํŠน๋ณ„ํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋Š” ์—†์œผ๋ฉฐ ๊ทธ๋ ‡๊ฒŒ ์ž‘๋™ํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๊ฒ€์ƒ‰ ์—”์ง„์— ๋ฌธ์ œ๊ฐ€ ์žˆ์œผ๋ฉด Github์™€ Twitter์—์„œ ํ•ด๊ฒฐํ•˜์ง€ ์•Š์€ ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

@armspkt ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์ด ์—ฌ๋Ÿฌ ๊ฐ€์ง€ <link rel="canonical"> ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒ€์ƒ‰ ๋ด‡์—๊ฒŒ ํฌ๋กค๋งํ•ด์•ผ ํ•˜๋Š” ํŽ˜์ด์ง€์™€ ๋‹ค๋ฅธ ๋ฒ„์ „์ด ์ค‘๋ณต๋œ ๊ฒƒ์œผ๋กœ ํ‘œ์‹œํ•ด์•ผ ํ•  ํŽ˜์ด์ง€๋ฅผ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ๋ฆฌ๋””๋ ‰์…˜์€ ์›น์‚ฌ์ดํŠธ์—์„œ SEO๋ฅผ ๋งŒ๋“œ๋Š” ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—์„œ ๋” ๋งŽ์€ ์ •๋ณด๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@ziserman ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์ด ์—ฌ๋Ÿฌ ๊ฐ€์ง€๋ผ๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์œ„ํ•ด ๋ฆฌ๋””๋ ‰์…˜ ์—†์ด ๋™์ผํ•œ URL์„ ์œ ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@Janpot https://github.com/nuxt-community/nuxt-i18n/issues/422

Nuxtjs์—๋Š” ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ช‡ ๊ฐ€์ง€ ์˜ต์…˜์ด ์žˆ์Šต๋‹ˆ๋‹ค(์ •์˜๋˜์ง€ ์•Š์Œ, true, false).

Nextjs๋„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ์„œ๋ฒ„ ์˜ต์…˜์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๊นŒ?

๋ฆฌ๋””๋ ‰์…˜์˜ ์ด์œ ๋Š” ๊ฒ€์ƒ‰ ์—”์ง„์— ์ค‘๋ณต ์ฝ˜ํ…์ธ ๊ฐ€ ํ‘œ์‹œ๋˜์ง€ ์•Š๋„๋ก ํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค. ์ •ํ™•ํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

@Janpot ์šฐ๋ฆฌ API์—๋Š” ๋งŽ์€ ๊ณณ์—์„œ ์Šฌ๋ž˜์‹œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ตœ์‹  ๋ฆด๋ฆฌ์Šค๋Š” ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ๊ฐ€ ์žˆ๋Š” URL(/api/test/ -> /api/test)์ด ์ผ์น˜ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ฐฑ์—”๋“œ์—์„œ ๋งŽ์€ 404๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

๋ชจ๋“  ์‚ฌ๋žŒ์—๊ฒŒ ํšจ๊ณผ๊ฐ€ ์žˆ์„์ง€๋Š” ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ ์ €์—๊ฒŒ ๋งž๋Š” ์ด ์†”๋ฃจ์…˜์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค. _app.js ํŒŒ์ผ์— ๋„ฃ์Šต๋‹ˆ๋‹ค.

static async getInitialProps(ctx) {
    const appProps = await App.getInitialProps(ctx);

    // Remove trailing slash
    const path = ctx.router.asPath,
            res = ctx.ctx.res;

    if (path.length > 1 && /\/$/.test(path)) {
        res.writeHead(301, {Location: path.slice(0, -1)})
        res.end();
    }

    return {...appProps};
}

@mlbonniec Next.js ์•ฑ์—์„œ ์‹ฌ๊ฐํ•œ ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ์œ ๋ฐœํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ท€ํ•˜์˜ ์˜๊ฒฌ์„ ์ตœ์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ตœ์‹  next@canary ๋ฒ„์ „์€ ์ด ๋ฒ„๊ทธ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์‹  ์—…๊ทธ๋ ˆ์ด๋“œํ•˜์‹ญ์‹œ์˜ค!

@mlbonniec Next.js ์•ฑ์—์„œ ์‹ฌ๊ฐํ•œ ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ์œ ๋ฐœํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ท€ํ•˜์˜ ์˜๊ฒฌ์„ ์ตœ์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ตœ์‹  next@canary ๋ฒ„์ „์€ ์ด ๋ฒ„๊ทธ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. ๋Œ€์‹  ์—…๊ทธ๋ ˆ์ด๋“œํ•˜์‹ญ์‹œ์˜ค!

๊ดœ์ฐฎ์•„์š”!
๊ทธ๋Ÿฌ๋‚˜ ์ด์ „์— ์—…๋ฐ์ดํŠธํ–ˆ๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
npm update ์™€ ํ•จ๊ป˜

์ตœ์‹  Next.js ์นด๋‚˜๋ฆฌ์•„๋กœ ๋ฒ„๊ทธ๊ฐ€ ์ˆ˜์ •๋˜์ง€ ์•Š์œผ๋ฉด ์ƒˆ ๋ฌธ์ œ๋ฅผ ์—ด์–ด ๊ฒ€ํ† ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์‹ญ์‹œ์˜ค. ๐Ÿ™

๋น ๋ฅธ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค. next export ๊ฐ€ ์žˆ๋Š” ํ”„๋กœ์ ํŠธ๋Š” ์ด ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๊นŒ? ํ›„ํ–‰ ์Šฌ๋ž˜์‹œ์˜ ๊ฐ ํŽ˜์ด์ง€์— ๋Œ€ํ•ด ์™„์ „ํžˆ ์ƒˆ๋กœ์šด ํŽ˜์ด์ง€๋ฅผ ์ƒ์„ฑํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ๋‚ด๋ณด๋‚ธ ์•ฑ์ด HTTP ๋ฆฌ๋””๋ ‰์…˜(๋˜๋Š” ์žฌ์ž‘์„ฑ)์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

next export ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ”„๋กœ์ ํŠธ์—๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์˜ ๋ชจ๋“  <Link /> ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์—…๋ฐ์ดํŠธ๋˜์ง€๋งŒ ์„œ๋ฒ„ ์ธก ๋ฆฌ๋””๋ ‰์…˜์—๋Š” ์ˆ˜๋™ ๊ตฌ์„ฑ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„๋ฆฌ์Šค ๋Œ€์ƒ ๋˜๋Š” next start ์™€ ํ•จ๊ป˜ ๋ฐฐํฌ๋œ ํ”„๋กœ์ ํŠธ๋Š” ์ด๋Ÿฌํ•œ ์„ค์ •์„ ์ž๋™์œผ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

@Timer ์ •์‹ ๋ฆด๋ฆฌ์Šค์— ๋„๋‹ฌํ•˜๋ฉด ์—ฌ์ „ํžˆ ์‹คํ—˜์  ์˜ต์…˜์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ?

@Timer ์ •์‹ ๋ฆด๋ฆฌ์Šค์— ๋„๋‹ฌํ•˜๋ฉด ์—ฌ์ „ํžˆ ์‹คํ—˜์  ์˜ต์…˜์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ?

์•„๋‹ˆ์š”, ์žˆ๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

trailingSlash ์˜ต์…˜์ด next export ๋Œ€ํ•ด ์ž‘๋™ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ /page/ ๋ฅผ /page (๋˜๋Š” ๊ทธ ๋ฐ˜๋Œ€๋กœ) ๋ฆฌ๋””๋ ‰์…˜ํ•˜๋Š” ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

trailingSlash ์˜ต์…˜์ด next export ๋Œ€ํ•ด ์ž‘๋™ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ /page/ ๋ฅผ /page (๋˜๋Š” ๊ทธ ๋ฐ˜๋Œ€๋กœ) ๋ฆฌ๋””๋ ‰์…˜ํ•˜๋Š” ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

๋‚ด๊ฐ€ ์•„๋Š” ํ•œ github ํŽ˜์ด์ง€์—๋Š” ๋ฆฌ๋””๋ ‰์…˜ ๊ธฐ๋Šฅ์ด ์—†์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ vercel.com์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ž‘๋™ํ•˜์ง€๋งŒ ์ทจ๋ฏธ ํ”„๋กœ์ ํŠธ(github ํŽ˜์ด์ง€์™€ ๊ฐ™์€)์—๋„ ๋ฌด๋ฃŒ์ž…๋‹ˆ๋‹ค.

next export ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ”„๋กœ์ ํŠธ์—๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์˜ ๋ชจ๋“  <Link /> ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์—…๋ฐ์ดํŠธ๋˜์ง€๋งŒ ์„œ๋ฒ„ ์ธก ๋ฆฌ๋””๋ ‰์…˜์—๋Š” ์ˆ˜๋™ ๊ตฌ์„ฑ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„๋ฆฌ์Šค ๋Œ€์ƒ ๋˜๋Š” next start ์™€ ํ•จ๊ป˜ ๋ฐฐํฌ๋œ ํ”„๋กœ์ ํŠธ๋Š” ์ด๋Ÿฌํ•œ ์„ค์ •์„ ์ž๋™์œผ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์•ˆ๋…•ํ•˜์„ธ์š” @Timer ๋” ์ž์„ธํžˆ ์„ค๋ช…ํ•ด ์ฃผ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ์ˆ˜๋™์œผ๋กœ ๊ตฌ์„ฑํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ•ฉ๋‹ˆ๊นŒ? ์—ฌ๊ธฐ ๋‚ด ์ƒํ™ฉ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚ด ์›น์‚ฌ์ดํŠธ์—์„œ๋Š” next-i18next ํ•ฉ๋‹ˆ๋‹ค. next build && next export ๋ฐฐํฌํ•œ ํ›„์—๋Š” ๋ชจ๋“  ๋‚ด๋ถ€ ๋งํฌ๊ฐ€ ์ž‘๋™ํ•˜์ง€๋งŒ URL์„ ์ˆ˜๋™์œผ๋กœ ์ž…๋ ฅํ•˜๋ฉด ๊ทธ ์ค‘ ์•„๋ฌด ๊ฒƒ๋„ ์ž‘๋™ํ•˜์ง€ ์•Š๊ณ  404 ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ ์—์„œ trailingSlash:true ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์œผ๋ฏ€๋กœ ์ˆ˜๋™์œผ๋กœ /pricing ํ•˜๋ฉด ์ด์ œ ์ž‘๋™ํ•˜์ง€๋งŒ /zh/pricing ๋Š” 404 ์˜ค๋ฅ˜๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰