[원티드 프리온보딩]CSR,SSR,Next.js-사전과제

조민수·2022년 10월 2일
1
post-thumbnail

2022-10-02
이번에 참 좋게도 원티드에서 제공하는 프론트엔드 프리온보딩 챌린지에 참여하게 되어
이와 같이 실제 교육에 앞서 사전과제를 이렇게 진행하게 되어
첫 번째로는 CSR의 개념과 SSR의 필요성과 최종적으로 Next.js의 도입의 필요성에 대하여 간단히 정리하고자 한다

🚩CSR이란 무엇인가

CSR 이전에 기본적으로 우리는 알다시피 웹은 클라이언트에서 요청을 보내게 되면 그 요청에 대한 응답을 서버에서 하게 되어 해당하는 html문서을 받게되어 페이지 전체를 그리는 방식으로 시작되었다

하지만 시대가 지날수록 정보의 양이 증가하게 됨에 따라 서버의 요청에 해야하는 것도 무척 많아지게 되었다.
그에 따른 매번 페이지 전체에 대한 요청은 사실 비효율적인 과정으로서 해당 방법이 생겨났다.

1.iframe의 도입으로 html안에 부분적으로 다른 html의 랜더링
2.XMLHttpRequest의 도입으로서 json형식으로 필요한 데이터만 받아와 js로 페이지에 랜더링
3.그 이후에는 Ajax의 도입으로 부분적으로 페이지를 업데이트 해내가는 방식으로 spa의 시초가 되었다

사실 나는 이미 React를 통한 개발을 해보면서 CSR에 대하여 이미 사용한 경험이 있다
그러면 정확히 CSR이 무엇인지 정의해보자

CSR은 클라이언트 사이드 랜더링의 줄임말로서 말 그대로 랜더링의 비중에 있어서 클라이언트 사이드의 비중을 굉장히 높여 가져가는 방식을 의미한다

기본적으로 우리가 알던 jsp나 아니면 Django에서 template을 통한 랜더링 방식은 csr의 반대되는 형식으로 서버에서 모든 html에 대한 랜더링 준비를 다 해줘서 보내주는 형식이다

당장 React에서의 구성에 있어서도 보면
맨위의 상단에 있는 index.html에서는 body에 해당하는 부분이 처음에는 랜더링 이전에는 텅 비어있는 것을
확인할 수가 있다

즉, 이 뜻은 저 비어있는 body부분에 js가 불러와지면서 각 컴포넌트단위로 랜더링이 된다는 얘기이다

구체적으로 말하자면 app.js파일을 서버에서 요청하여 갖고와
app.js+추가적인 데이터 json등을 받아와 동적으로 화면을 구성하게 된다는 것이다

이 때 추가적인 json데이터는 외부에서 가져오는 api에 대한 결과가 될 수 있겠다

위의 그림을 보면 csr은 서버로부터 필요한 html,css,js를 불러오게 되고 html내부에서 작성되는 것이 아닌 js파일을 읽어내면서 클라이언트에서 직접 랜더링하는 방식으로 페이지를 랜더링하는 것이다.
(실제로는 웹팩에 의한 bundle.js를 통한 하나의 파일을 통해 불러오게 된다)

🙋‍♂️CSR의 장점 및 단점

<장점>

csr은 서버의 의존도 없이 클라이언트 혼자서만 그릴 수 있게 되므로 빠른 화면전환이나 인터랙션이 가능해지게 된다

왜냐하면? 매번 url이 달라지더라도 실제 라우터에 의한 작업으로 다른 컴포넌트를 보여주면 되므로 서버에 의한 요청은 하나도 생기지 않게되니까?

<단점>

1.csr은 기본적으로 초반에 너무 느리다,,,

앞서 말했다시피 csr은 초반에 bundle.js를 통해 랜더링에 필요한 모든 파일들을 하나의 번들링된 형태로 가져오게 된다 따라서 이러한 형태는 초반에 랜더링하는데 있어서 시간이 요구되게 하여 그 사이에 빈화면과 로딩화면을 볼 수 있게 된다...

2.SEO에 어려움을 겪게 된다,,,

SEO는 Search Engine Optimization의 약자로서 웹 페이지 검색엔진이 자료를 수집하고 순위를 매기는 방식을 통해 검색결과의 상위에 나올 수 있도록 하게 하는 작업을 의미한다.이 때 검색엔진최적화 과정에 있어서 페이지의 구성요소들을 가지고 진행을 하게 되는데 이 때 csr은 페이지의 구성요소가 처음에 body태그부분이 비어있게 되어 해당요소를 살피고 최적화시키는데 어려움을 겪게 됩니다

🤷‍♂️하지만 어찌보면 csr의 SEO가 어려운 특징은 html 문서에 미리 다 들어가있지 않은 상태로 오기에
데이터를 보호해야 하는 서비스인 경우에는 의도적으로 더욱 도움이 될 수도 있겠다!

🚩SSR이 그럼 왜 spa에서 필요하게 될까?

SSR은 서버사이드랜더링으로서 사실 우리가 흔히 아는 방식의 랜더링(jsp/servlet,php,Django template)으로 서버로부터 필요한 html 랜더링준비를
마친 상태로 클라이언트로 내려주게 되어 클라이언트는 서버에서 받은 html을 랜더링 하게 되주면 된다

또한 필요한 js파일들을 같이 넘겨주어 동적인 화면구성을 해주게 되는 간단한 방식이다

그럼 왜 대체 무슨 장점이 있길래..?

첫페이지의 랜더링에 빠른 속도-CSR의 문제점을 해결시켜줌

SSR은 기본적으로 CSR이 갖고 있던 문제점인 초반에 bundle.js을 가져오는 지연되는 시간에 대하여 바로 서버에서 틀이잡힌 html을 바로 전달받기에 첫페이지랜더링에 빠르게 된다

SEO에 문제점을 해결시켜준다-CSR의 문제점 해결

이미 html문서는 비어있지않고 페이지의 구성요소들로 차있기에 검색엔지최적화에서도 수월할 수 밖에 없다

<단점>

Blinking Issue가 발생하게 된다..?

말그대로 깜빡이는 이슈가 생긴다는 것 이다.생각해보면 전체적인 웹사이트를 매 이벤트 발생시마다 새롭게 불러오기에 ux측면에서 아주 깜빡깜빡하는 것처럼 이런 화면전환이 보이게 된다는 것이다

서버의 과부하

말그대로 매번 부분적인 요청에 의한 것에도 전체적인 재요청과 비록 json을 통한 api요청을 할지언정 사용자가 많아지면 서버에는 그만큼 각 클라이언트들에게 랜더링을 할 준비를 하고 내려줘야하니 그만큼 부하가 생기게 된다

인터랙트를 하는데까지 기다려야할 수 있다,,,

가장 ssr의 치명적인 단점으로서 js파일을 통한 동적인 화면구성을 진행할 때 첫페이지에 대한 js파일을 불러오기는 하였지는만 그 외의 동적인 화면구성에 대한 이벤트에 대해서는 js를 불러오지 못하였기에 아무리 사용자가 버튼을 눌러도 원하는 이벤트에 대한 결과가 진행이 안 될 수 있다.
즉 화면은 랜더링되어서 보이지만 해당 이벤트리스너에 대한 코드는 없는 상황이라고 보면 된다

즉,CSR이 가지고 있던
첫 지연시간의 향상과 seo를 위한 향상에 있어서는 ssr이 필요하게 된다!!

SEO측면에서 보면 ssr은 미리 html이 구성되어 있기에 퍼블릭데이터가 많고 정적인 컨텐츠가 많다면 더욱 도움이 될 것 이다

🙋‍♂️그럼 csr과 ssr의 각 장점들만 뽑아서 만들수 있는거 없을까?..

그래서 바로 생겨난게 Next.js인 것 이다.

Next.js가 생겨난 이유는 단순하다 말그대로

흐음,...처음에 index.html을 불러올때에는 ssr처럼 미리 다 html랜더링 준비를 마쳐서 클라이언트로 내려주어 초기속도를 향상시켜주고 나중에 페이지를 바꿀때에는 csr처럼 서버에 의존하지 않고 클라이언트단에서 해주면 되지 않을까..?

리액트를 한번 더 프레임워크로 감싼 형태로 생각하면 된다
(물론 csr의 프레임워크는 Angular,Vue,React 등이 있음)

😜그래서 배워볼려한다 Next.js,,

🖥Next.js를 시작해보자

Next.js를 시작하기 위해

npx next-create-app myapp

을 통해 새롭게 app을 하나 만들어주고 실제 실행될 때의 대략 어떤구성으로 서버가 돌아가기 시작하는지 코드를 NextJs repository에서 한 번 봐보자


실제 package.json의 scirpt 부분을 보면 yarn start를 해줄경우
next start에 대한 script가 시작된다는 것을 알 수 있다

그럼 next start는 어디서부터 시작되는지 알아보기 전에 해당 스크립트 명령어들은 어떤식으로 정의가 되었는지 볼려면

기본적으로 package.json에서 호출되는 스크립트들은 현재디렉토리의

node_modules/.bin 

(Nextjs 레포지토리상으로는 packages/next/bin/)
하위 디렉토리에서의 모든 스크립트를 이름으로 호출할 수 있게 된다

🖥script가 동작되는 원리

실제 script가 npm상 동작되는 원리는 npm run이나 yarn start를 해줄 경우 자동으로 새로운 shell
이 생성이 되게 된다(우리가 아는 Bash shell)이라고 생각하면 된다
그러면 npm run을 통한 새롭게 생성된 쉘에서는 node_modules/.bin 하위 디렉토리에 대한 PATH 변수를 추가해주고 실행이 끝나면 다시 원상태로 돌려주게 된다

즉 PATH변수에 추가가 되었기 때문에 더이상 파일경로에 대한 구체적인 명시가 아니더라두
해당 파일의 이름으로서 script가 실행되는 것 이다.
원래는

	"./node_modules/.bin/next start"

이런식이 맞는 것이지만 우리는 더이상 구체적으로 해주지 않아도

	next start

이런 접근이 가능해지게 되는 것이다

그럼 한번 해당 패키지로 내려가보면

//packages/next/bin/next.ts의 모습

#!/usr/bin/env node
import * as log from '../build/output/log'
import arg from 'next/dist/compiled/arg/index.js'
import { NON_STANDARD_NODE_ENV } from '../lib/constants'
import { commands } from '../lib/commands'
;['react', 'react-dom'].forEach((dependency) => {
  try {
    // When 'npm link' is used it checks the clone location. Not the project.
    require.resolve(dependency)
  } catch (err) {
    console.warn(
      `The module '${dependency}' was not found. Next.js requires that you include it in 'dependencies' of your 'package.json'. To add it, run 'npm install ${dependency}'`
    )
  }
})
//실제 해당 부분에 대한 콘솔같은 것을 보면 react와 react-dom에 대한 dependency를 불러와
//resolove를 해주고 아니면 에러가 날때 해당 depenedency가 없다는 에러메시지를 익숙한 모습을 볼수있게된다
const defaultCommand = 'dev'
const args = arg(
  {
    // Types
    '--version': Boolean,
    '--help': Boolean,
    '--inspect': Boolean,

    // Aliases
    '-v': '--version',
    '-h': '--help',
  },
  {
    permissive: true,
  }
)

// Version is inlined into the file using taskr build pipeline
if (args['--version']) {
  console.log(`Next.js v${process.env.__NEXT_VERSION}`)
  process.exit(0)
}

//우리가 뒤에 --version같은 것을 쓰게 될때 나타나는 해당 next.js의 버젼을 보여주는 것이라고 이해하면됌

// Check if we are running `next <subcommand>` or `next`
const foundCommand = Boolean(commands[args._[0]])

// Makes sure the `next --help` case is covered
// This help message is only showed for `next --help`
// `next <subcommand> --help` falls through to be handled later
if (!foundCommand && args['--help']) {
  console.log(`
    Usage
      $ next <command>

    Available commands
      ${Object.keys(commands).join(', ')}

    Options
      --version, -v   Version number
      --help, -h      Displays this message

    For more information run a command with the --help flag
      $ next build --help
  `)
  process.exit(0)
}

const command = foundCommand ? args._[0] : defaultCommand
const forwardedArgs = foundCommand ? args._.slice(1) : args._

if (args['--inspect'])
  throw new Error(
    `--inspect flag is deprecated. Use env variable NODE_OPTIONS instead: NODE_OPTIONS='--inspect' next ${command}`
  )

// Make sure the `next <subcommand> --help` case is covered
if (args['--help']) {
  forwardedArgs.push('--help')
}

const defaultEnv = command === 'dev' ? 'development' : 'production'

const standardEnv = ['production', 'development', 'test']

if (process.env.NODE_ENV) {
  const isNotStandard = !standardEnv.includes(process.env.NODE_ENV)
  const shouldWarnCommands =
    process.env.NODE_ENV === 'development'
      ? ['start', 'build']
      : process.env.NODE_ENV === 'production'
      ? ['dev']
      : []

  if (isNotStandard || shouldWarnCommands.includes(command)) {
    log.warn(NON_STANDARD_NODE_ENV)
  }
}

;(process.env as any).NODE_ENV = process.env.NODE_ENV || defaultEnv
;(process.env as any).NEXT_RUNTIME = 'nodejs'

// In node.js runtime, react has to be required after NODE_ENV is set,
// so that the correct dev/prod bundle could be loaded into require.cache.
const { shouldUseReactRoot } = require('../server/utils')
if (shouldUseReactRoot) {
  ;(process.env as any).__NEXT_REACT_ROOT = 'true'
}

// x-ref: https://github.com/vercel/next.js/pull/34688#issuecomment-1047994505
if (process.versions.pnp === '3') {
  const nodeVersionParts = process.versions.node
    .split('.')
    .map((v) => Number(v))

  if (
    nodeVersionParts[0] < 16 ||
    (nodeVersionParts[0] === 16 && nodeVersionParts[1] < 14)
  ) {
    log.warn(
      'Node.js 16.14+ is required for Yarn PnP 3.20+. More info: https://github.com/vercel/next.js/pull/34688#issuecomment-1047994505'
    )
  }
}

// Make sure commands gracefully respect termination signals (e.g. from Docker)
// Allow the graceful termination to be manually configurable
if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
  process.on('SIGTERM', () => process.exit(0))
  process.on('SIGINT', () => process.exit(0))
}

commands[command]()
  .then((exec) => exec(forwardedArgs))
  .then(() => {
    if (command === 'build') {
      // ensure process exits after build completes so open handles/connections
      // don't cause process to hang
      process.exit(0)
    }
  })

해당 맨 마지막 부분을 보면 commands에 대한 array에서 해당 command를 접근하여
해당 명령어를 exec로 실행해주는 것을 볼 수 있다

그럼 command가 어디서 왔는지 확인을해보면

	import { commands } from '../lib/commands'

이렇게 되어있다 그럼 해당 폴더로 이동해보자

(현재경로:node_modules/next/dist/lib/command.js)
(NextJs레포지토리:packages/next/lib/commands.ts)

commands.js에서는 실제로 이미 우리가 스크립트상에서 쓰고있던 다양한 명령어들이 여기서 정의가 되어있는 것을 확인할 수 있다.

해당 commands들은 화살표함수로서 ../cli/명령어파일들에서 동적으로 require하여 해당모듈에 nextBuild처럼 각각의 구현된 함수들을
프로미스객체의 resolve로 넘기는 값으로 사용한다

그럼 start할시 resolve로 넘기는 값인 start에 대하여 또 따라가보자

(현재경로:node_modules/next/dist/cli/next-start.js)
(레포지토리경로상:packages/next/cli/next-start.ts)

해당 코드에서 보면 불러서 가져다쓰는 nextStart부분을 보면
startServer라는 함수를 실행하는 것을 볼 수가 있다

그럼 startServer를 또 구현하고 있는
start-server.js에 대한 코드로 또 올라가보자

(현재경로:node_modules/next/dist/server/lib/start-server.js)
(레포지토리경로상:packages/next/server/lib/start-server.ts)

해당함수는 최종적으로 Promise객체를 반환하게되면서 app에대한 정보(hostname,customserver,httpserver,port )를 startServer에 대하여 then()으로 받아냄으로서 최종적으로 해당 서버가 켜지는 원리인 것 같다

하지만 실제 start-server.ts에서 좀 더 자세히봐보면

next함수를 통하여 app이라는것을 만들어주는 것을 확인할 수 있는데 좀 더 들어가보면


최종적으로 default createServer를 next.ts에서는 넘겨주는 것을 알 수 있다

(참고)여기서 새롭게 안 것
import next from "../next"에서 이렇게 했지만
실제로 next.ts에서는 createServer로 export를 하는 것을 알 수 있는데 이와 같이 export default로 하면 무조건 해당파일에서는 하니의 메소르를 export를 하는 것을 보장하기에 가져올때 다른 별칭으로 가져올 수 있다.하지만 default를 하지 않을경우에는 별칭을 쓸려면 as를 사용하기

다시 돌아와 createServer에서는 NextServer를 반환하는데
NextServer는 해당 Next.ts에 구현되어 있는 하나의 클래스이다


근데 잘 보면 해당 클래스 내부에는 이와 같이

html을 랜더링 해주는 메소드들이 정의된 것을 확인할 수 있다

따라서 느낌적으로 해당 NextServer를 반환하면 서버에서 저런 메소드를 써서 랜더링을 해주지 않을까라는 생각을 해볼수있다
이게 바로 ssr의 모습!!
즉 첫랜더링시에는 서버에서 랜더링을 맡아줌을 알 수 있게 된거다

profile
컬러감이 있는 프론트엔드개발자

0개의 댓글