보통 리액트 프로젝트를 생성할 때 create-react-app
명령어로 생성하여 프로젝트를 실행하고 개발하면서, 개발 서버가 어떻게 어떤 원리로 실행되는지에 대한 의문을 품지 않았습니다.
그래서 개발 서버가 어떻게 실행되는지에 대한 질문을 받았을 때, 대답을 하지 못하는 저의 모습을 보고 이번 기회에 살펴보기로 결심했고 관련된 내용을 정리하였습니다.
우리는 보통 리액트 프로젝트를 생성할 때 create-react-app
명령어로 생성합니다.
이렇게 create-react-app
명령어로 애플리케이션을 생성하면 개발 환경 설정, 빌드 도구, 기본 디렉터리 구조 등을 자동으로 설정해 줍니다.
이후 npm start
명령어를 실행하면, package.json
파일에 정의된 스크립트가 실행되어 브라우저에서 http://localhost:3000
을 입력하면 리액트 애플리케이션이 구동되는 것을 확인할 수 있습니다.
npm start
명령어를 입력했을 때동영상이 업로드되지 않아 사진으로 대체하지만, 시작할 때 위의 문구가 빠르게 출력되었다가 사라지는 것을 확인할 수 있고, npm start
명령어를 입력하면 개발 서버가 시작됩니다
그렇다면 React 애플리케이션의 개발 서버는 어떻게 실행되는지 알아보겠습니다.
React가 이렇게 http://localhost:3000
에서 애플리케이션을 실행할 수 있는 이유는 create-react-app
이 제공하는 react-scripts
와 Webpack Dev Server
때문입니다.
create-react-app
명령어로 리액트 애플리케이션을 생성하면, 프로젝트는 react-scripts
패키지에 의존하며 이 패키지는 개발 환경 설정과 빌드 프로세스를 관리하며, react-scripts
는 Webpack Dev Server
를 사용하여 개발자가 쉽게 개발 서버를 시작하고 변경사항을 빠르게 반영하여 정적 파일을 제공할 수 있도록 도와줍니다.
즉, npm start
명령어로 애플리케이션의 개발 서버를 실행하면, Webpack Dev Server
가 소스 코드를 번들링하고, 이를 메모리에 저장하여 빠르게 제공하는 방식으로 동작합니다.
react-scripts
는 create-react-app(CRA)
의 대부분의 기능을 수행하는 패키지로 개발, 빌드, 테스트, 배포 등 다양한 작업을 관리합니다.
이 패키지는 프로젝트의 복잡한 설정과 구성을 자동으로 관리하여 개발자가 신경 쓰지 않고 코드 작성에만 집중할 수 있도록 도와줍니다.
react-scripts는 package.json 파일안에 있으며, 여기에 여러 스크립트가 정의되어 있고, 추가할 수 있습니다.
npm start
의 명령어로 개발 서버를 실행npm run build
명령어로 애플리케이션을 프로덕션용으로 빌드npm test
명령어로 테스트를 실행eject
로 설정 파일을 숨겨 개발자가 설정에 신경 쓰지 않고 개발에 집중할 수 있도록 함. 프로젝트 폴더로 가져와서 직접 수정할수도 있음.여기서 npm start
를 실행하면 react-scripts start
가 호출되어 webpack-dev-server
를 사용하여 개발 서버를 실행하게 됩니다.
Webpack Dev Server는 Webpack
으로 번들링된 파일을 제공하는 개발 서버로, 실시간으로 파일 변경을 감지하고, 변경된 파일을 브라우저에 반영하여 빠른 개발 사이클을 지원합니다. (webpack devServer 공홈)
http://localhost:3000
에서 실행Webpack Dev Server는 주로 Webpack 설정 파일인 webpack.config.js
를 통해 설정됩니다. (create-react-app
을 사용하면 이 설정이 react-scripts
에 의해 자동으로 관리되기 때문에 직접 파일을 수정할 필요가 없습니다)
// webpack.config.js 예시
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/'
},
// devServer 옵션은 Webpack Dev Server 동작 방식을 정의
devServer: {
contentBase: path.join(__dirname, 'public'),
compress: true,
port: 3000,
hot: true,
historyApiFallback: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
devServer 옵션은 Webpack Dev Server의 동작 방식을 정의합니다.
contentBase
: 정적 파일의 위치를 정의compress
: 모든 항목에 대해 gzip
압축을 활성화port
: 개발 서버가 실행될 포트를 지정hot
: HMR(Hot Module Replacement)을 활성화historyApiFallback
: HTML5 History API 기반의 라우팅을 사용할 때, 404 응답을 index.html로 대체즉, react-scripts start
는 스크립트를 실행하여 애플리케이션 실행에 필요한 환경 변수를 설정하고, Webpack Dev Server
시작하여 애플리케이션이 로컬에서 실행할 수 있도록 개발 서버를 시작하고, 소스 파일의 변경을 감시하고 변경될 때마다 자동으로 애플리케이션을 다시 빌드하고 기본 브라우저에서 http://localhost:3000
을 자동으로 열어주는 역할을 합니다.
npm start
로 리액트 애플리케이션을 실행npm start
명령어를 실행하면 Node.js
는 package.json
파일에 정의된 scripts
섹션의 start
명령어를 실행합니다.react-scripts start
명령어는 내부적으로 Webpack Dev Server를 설정하고 실행합니다.Webpack Dev Server
는 Webpack을 사용하여 프로젝트의 모든 JavaScript, CSS 파일 등을 번들링하고, 번들링된 파일을 메모리에 저장하며 http://localhost:3000
에서 개발 서버를 시작합니다.http://localhost:3000
을 입력하고 엔터를 입력합니다.localhost
는 특별한 도메인 이름으로 로컬 호스트를 가리키기 때문에 DNS 해석 과정 없이 바로 로컬 네트워크 인터페이스를 가리킨 후, 브라우저는 localhost
를 IP 주소로 해석하고 포트 번호 3000
번으로 연결을 시도합니다.Webpack Dev Server
는 브라우저로부터 요청을 수신하여 브라우저가 요청한 경로에 해당하는 번들 파일을 메모리에서 검색합니다.index.html
파일을 처리합니다.index.html
파일에 포함된 <script>
태그를 통해 번들된 JavaScript 파일을 로드하고 React 애플리케이션이 실행되어 브라우저는 UI를 사용자에게 표시합니다.http://localhost:3000
을 입력하여 리액트 애플리케이션을 실행했을 때http://localhost:3000
을 입력하고 엔터를 입력localhost
는 특별한 도메인 이름으로 로컬 호스트를 가리키기 때문에 DNS 해석 과정 없이 바로 로컬 네트워크 인터페이스를 가리킨 후, 브라우저는 localhost
를 IP 주소로 해석하고 포트 번호 3000
번으로 연결을 시도합니다.Webpack Dev Server
가 실행되지 않아서 응답할 서버가 없기 때문에 에러가 발생합니다.ps aux | grep node
명령어를 통해 실행 중인 프로세스 목록에서 webpack-dev-server
또는 node
를 필터링하여 확인할 수 있습니다.ps aux | grep node
로 검색하면 너무 많은 프로세스가 나와서 lsof -i :3000
명령어로 포트 3000을 사용 중인 프로세스를 검색하여 확인할 수 있습니다.만약 브라우저에서 http://localhost:3000
을 입력하지 않고 React 애플리케이션의 build 파일인
index.html
을 클릭 했을 때는 제대로 애플리케이션이 구동하는지 확인해보면 빈 화면만 출력되는 것을 알 수 있습니다.
위의 이미지를 보면 javascript 파일을 제대로 불러오지 못하는 것을 확인할 수 있습니다.
위의 이미지를 보면 index.html
의 Request URL
의 경로가 (file://
)로 시작하는 것 알 수 있는데 React 애플리케이션은 빌드될 때, 자바스크립트와 CSS 파일 등은 상대 경로로 지정되기 때문에 브라우저에서 파일 시스템 경로(file://
)로 직접 접근하면 이러한 경로를 제대로 해석하지 못합니다.
예를들어 <script defer="defer" src="/static/js/main.3dd63bcb.js"></script>
의 경우 서버 환경에서만 제대로 동작하고 로컬 파일 시스템에서 열 때는 이 경로가 올바르게 해석되지 않습니다.
즉, npm start
명령어로 실행되는 Webpack Dev Server는 JS 파일을 번들링하고, 이를 클라이언트에게 제공하는 역할을 하기 때문에 이 서버가 없으면 브라우저는 필요한 자원을 로드하지 못합니다.
브라우저의 보안 정책으로 인해, 파일 시스템에서 직접 열 경우 Cross-Origin Resource Sharing (CORS) 문제로 인해 외부 자원을 로드하지 못하기 때문에 자바스크립트 파일들이 제대로 로드되지 않을 수 있습니다.
즉 이러한 문제를 해결하기 위해 React 애플리케이션을 개발할때는 npm start
명령어를 사용하여 Webpack Dev Server를 통해 개발 서버를 실행하는것이 가장 좋습니다. 그 이유는 모든 필요한 자원을 제공하며, 로컬 파일 시스템의 문제를 피할 수 있기 때문입니다.
Vite도 npm run dev
명령어를 사용하여 개발 서버를 시작할 수 있습니다.
npm run dev
명령어를 실행하면, Vite는 개발 서버를 시작
Vite는 개발 환경에서 번들링을 하지 않으며, 브라우저의 ES Module을 직접 사용하여 모듈을 로드하기 때문에 초기 빌드 시간을 단축할 수 있습니다.
코드가 변경되면 HMR로 인해 변경된 모듈만 브라우저에게 전달하기 때문에 전체 페이지를 새로고침 하지 않고도 변경사항을 즉시 반영합니다.
브라우저가 모듈을 요청하면 Vite 개발 서버는 해당 모듈을 찾아 응답하고 최신 JavaScript 문법을 지원하지 않는 브라우저를 위해 필요한 경우 ES Module로 변환하거나 디버깅을 위해 소스 맵을 제공합니다.
npm run build
명령어로 Rollup을 사용하여 최적화된 번들을 생성합니다.
Rollup 은 모듈 번들러로 프로덕션 빌드 시 사용하는 도구이며, JavaScript 모듈을 최적화된 하나의 파일이나 여러 파일로 번들링하여 배포를 위해 코드 크기를 최소화하고 성능을 향상시킨다.
package.json
파일에 정의된 scripts
섹션의 start
명령어를 실행하여 next dev
명령어가 실행된다.next dev
는 내부적으로 Webpack Dev Server
를 설정하고 실행한다.webpack dev server
는 파일들을 번들링하고 메모리에 저장하여 http://localhost:3000
에서 개발 서버를 시작한다.http://localhost:3000
을 입력하면 localhost
는 특별한 도메인 이름으로 로컬 호스트를 가리키기 때문에 DNS 해석 과정 없이 바로 로컬 네트워크 인터페이스를 가리킨 후, 브라우저는 localhost
를 IP 주소로 해석하고 포트 번호 3000
번으로 연결을 시도한다.Next.js
서버는 페이지 컴포넌트(예: pages/index.js
)를 렌더링한다.webpack이 많은 일들을 대신해 준다는 것은 인지만 하고 있었지 정확히 어떤 일들을 처리해 주고, 어떤 원리로 개발서버가 시작되고 코드가 바로 반영되는지 의문을 품지 않았던 저의 모습을 되돌아보게 해주는 기회가 되어서 좋았고, 추후에는 조금 더 나아가서 번들링에 대해서도 조금 더 깊이 있게 토끼굴을 파봐야겠다고 생각했습니다.
우와... next.js에서 컴포넌트가 서버에서 렌더링된다는 게 어떤 의미인지 아리송했는데 이 글 덕분에 제대로 이해할 수 있었어요 정말 감사합니다!