React와 Next.js로 시작하는 모던 웹 개발: 초보자를 위한 완벽 가이드

웹 개발을 막 시작하려는 분들이라면 React와 Next.js라는 이름을 한 번쯤 들어보셨을 겁니다. 하지만 “왜 이 기술들을 배워야 하는가?”라는 질문에 명확한 답을 찾기는 쉽지 않습니다. 저 역시 웹 개발을 시작했을 때 수많은 프레임워크와 라이브러리 사이에서 어떤 것을 선택해야 할지 막막했던 기억이 있습니다.

이 글에서는 React와 Next.js가 무엇인지, 왜 현대 웹 개발에서 이렇게 중요한 위치를 차지하게 되었는지, 그리고 실제로 어떻게 시작할 수 있는지를 단계별로 자세히 설명하겠습니다. 단순히 설치 방법만 알려드리는 것이 아니라, 각 개념의 의미와 실무에서 어떻게 활용되는지까지 함께 이해할 수 있도록 구성했습니다.

React는 왜 필요한가: 웹 개발 패러다임의 변화

전통적인 웹사이트는 서버에서 완성된 HTML 페이지를 만들어 브라우저로 전송하는 방식이었습니다. 사용자가 링크를 클릭할 때마다 새로운 페이지를 서버에서 받아와야 했죠. 이 방식은 간단하지만 사용자 경험 측면에서 여러 한계가 있었습니다.

예를 들어 이메일 서비스를 생각해봅시다. 받은 편지함에서 메일 하나를 클릭했을 때, 전체 페이지가 새로고침되면서 깜빡인다면 어떨까요? 사이드바, 헤더, 모든 요소가 다시 로드되는 것은 불필요한 작업입니다. 실제로는 메일 내용 부분만 업데이트되면 충분하니까요.

React는 바로 이런 문제를 해결하기 위해 만들어졌습니다. React의 핵심 철학은 “컴포넌트 기반 개발”과 “가상 DOM”입니다. 웹 페이지를 작은 재사용 가능한 조각들로 나누고, 실제로 변경이 필요한 부분만 효율적으로 업데이트하는 방식입니다.

컴포넌트라는 개념을 이해하기 위해 레고 블록을 떠올려보세요. 각각의 블록은 독립적이지만 서로 조합되어 복잡한 구조물을 만들 수 있습니다. React 컴포넌트도 마찬가지입니다. 버튼 컴포넌트, 입력 폼 컴포넌트, 네비게이션 바 컴포넌트 등을 만들고, 이들을 조합해서 완전한 애플리케이션을 구성합니다.

가상 DOM은 React가 성능을 최적화하는 핵심 메커니즘입니다. 실제 웹 페이지의 DOM을 직접 조작하는 대신, React는 메모리 상에 가상의 DOM 트리를 유지합니다. 데이터가 변경되면 React는 먼저 가상 DOM에서 변경사항을 계산하고, 실제 DOM에는 정말 필요한 최소한의 변경만 적용합니다. 이는 마치 건축가가 실제 건물을 짓기 전에 청사진으로 여러 시뮬레이션을 해보는 것과 비슷합니다.

Next.js가 React의 한계를 보완하는 방법

React는 강력한 UI 라이브러리이지만, 프로덕션 수준의 웹 애플리케이션을 만들기 위해서는 추가적인 설정과 도구가 필요합니다. 라우팅 시스템, 서버 사이드 렌더링, 이미지 최적화, 코드 분할 등을 직접 구성해야 하죠. 이 과정은 복잡하고 시간이 많이 걸립니다.

Next.js는 React를 기반으로 하면서도 이러한 복잡한 설정을 대신해주는 프레임워크입니다. 특히 검색 엔진 최적화와 초기 로딩 속도라는 두 가지 중요한 문제를 해결합니다.

순수 React 애플리케이션은 기본적으로 클라이언트 사이드 렌더링 방식입니다. 브라우저가 빈 HTML을 받고, JavaScript를 다운로드하고 실행해서 콘텐츠를 만들어냅니다. 이 과정에는 시간이 걸리며, 검색 엔진 크롤러는 JavaScript가 실행되기 전의 빈 페이지만 보게 될 수 있습니다.

Next.js는 서버 사이드 렌더링을 제공합니다. 사용자가 페이지를 요청하면 서버에서 React 컴포넌트를 실행하여 완성된 HTML을 만들어 전송합니다. 브라우저는 즉시 콘텐츠를 표시할 수 있고, 검색 엔진도 완전한 페이지 내용을 읽을 수 있습니다. 이후 JavaScript가 로드되면 페이지는 인터랙티브하게 작동하기 시작합니다. 이를 하이드레이션이라고 합니다.

또한 Next.js는 정적 사이트 생성 기능도 제공합니다. 블로그처럼 내용이 자주 변하지 않는 페이지는 빌드 타임에 미리 HTML을 생성해둘 수 있습니다. 이렇게 하면 서버가 매번 페이지를 렌더링할 필요 없이 미리 만들어진 HTML 파일을 바로 전송하므로 응답 속도가 매우 빠릅니다.

개발 환경 설정: 단계별 가이드

이제 실제로 Next.js 프로젝트를 시작하는 방법을 알아보겠습니다. 먼저 필요한 도구들을 설치해야 합니다.

Node.js는 JavaScript 런타임 환경으로, 브라우저 밖에서도 JavaScript를 실행할 수 있게 해줍니다. Next.js는 Node.js 위에서 동작하므로 반드시 설치해야 합니다. Node.js 공식 웹사이트에서 LTS 버전을 다운로드하여 설치하세요. LTS는 Long Term Support의 약자로, 안정적이고 장기간 지원되는 버전입니다.

설치가 완료되었는지 확인하려면 터미널이나 명령 프롬프트를 열고 다음 명령어를 입력하세요. node -v를 입력하면 설치된 Node.js 버전이 표시되고, npm -v를 입력하면 Node Package Manager 버전이 표시됩니다. npm은 Node.js와 함께 자동으로 설치되는 패키지 관리 도구입니다.

Next.js 프로젝트를 생성하는 가장 쉬운 방법은 공식 CLI 도구를 사용하는 것입니다. 원하는 폴더에서 터미널을 열고 다음 명령어를 실행하세요. npx create-next-app@latest my-first-app이라고 입력하면 됩니다. 여기서 my-first-app은 프로젝트 이름이므로 원하는 이름으로 변경할 수 있습니다.

설치 과정에서 몇 가지 질문이 나옵니다. TypeScript를 사용할지 물어보는데, TypeScript는 JavaScript에 타입 시스템을 추가한 언어입니다. 초보자라면 처음에는 No를 선택하고 JavaScript로 시작하는 것을 추천합니다. ESLint 사용 여부를 물으면 Yes를 선택하세요. ESLint는 코드의 잠재적인 오류를 찾아주는 도구입니다. Tailwind CSS 사용 여부도 물어보는데, 이는 유틸리티 기반의 CSS 프레임워크입니다. 처음에는 No를 선택하고 기본 CSS부터 익히는 것이 좋습니다.

프로젝트 생성이 완료되면 해당 폴더로 이동합니다. cd my-first-app이라고 입력하세요. 그리고 npm run dev를 실행하면 개발 서버가 시작됩니다. 브라우저에서 localhost:3000을 열면 Next.js 기본 시작 페이지를 볼 수 있습니다.

Next.js 프로젝트 구조 이해하기

생성된 프로젝트 폴더를 열어보면 여러 파일과 폴더가 있습니다. 각각의 역할을 이해하는 것이 중요합니다.

app 폴더는 Next.js 13 버전 이후 도입된 새로운 방식입니다. 이 폴더 안의 구조가 곧 웹사이트의 URL 구조가 됩니다. 예를 들어 app/about/page.js 파일을 만들면 자동으로 /about 경로가 생성됩니다. 이를 파일 시스템 기반 라우팅이라고 합니다.

page.js 파일은 각 경로의 실제 페이지 콘텐츠를 담고 있습니다. layout.js 파일은 여러 페이지에서 공통으로 사용되는 레이아웃을 정의합니다. 예를 들어 모든 페이지에 동일한 헤더와 푸터를 넣고 싶다면 layout.js에 작성하면 됩니다.

public 폴더에는 이미지나 파비콘 같은 정적 파일들을 넣습니다. 이 폴더의 파일들은 그대로 웹 루트에 제공됩니다. 예를 들어 public/logo.png 파일은 /logo.png로 접근할 수 있습니다.

node_modules 폴더에는 프로젝트가 사용하는 모든 외부 라이브러리가 저장됩니다. 이 폴더는 직접 수정하지 않으며, npm이 자동으로 관리합니다. package.json 파일에는 프로젝트의 메타데이터와 의존성 목록이 기록되어 있습니다.

next.config.js 파일은 Next.js의 설정을 커스터마이징할 때 사용합니다. 처음에는 기본 설정으로 충분하지만, 나중에 이미지 도메인 설정이나 환경 변수 등을 추가할 수 있습니다.

첫 번째 페이지 만들기: 실습으로 배우는 React 컴포넌트

이제 간단한 페이지를 직접 만들어보겠습니다. app 폴더 안에 page.js 파일을 열면 기본 코드가 있습니다. 이를 모두 지우고 새로운 코드를 작성해봅시다.

React 컴포넌트는 함수로 작성됩니다. 함수 이름은 대문자로 시작해야 한다는 규칙이 있습니다. 이 함수는 JSX라는 특별한 문법을 반환하는데, JSX는 JavaScript 안에 HTML을 작성할 수 있게 해주는 확장 문법입니다.

간단한 홈 페이지 컴포넌트를 만들어봅시다. export default function Home()이라고 함수를 정의하고, return 문 안에 JSX를 작성합니다. div 태그로 전체를 감싸고, h1 태그로 제목을 넣고, p 태그로 설명 문구를 넣습니다. 중요한 점은 하나의 부모 요소로 모든 것을 감싸야 한다는 것입니다. 여러 형제 요소를 바로 return할 수 없습니다.

JSX 안에서 JavaScript 표현식을 사용하려면 중괄호를 사용합니다. 예를 들어 현재 날짜를 표시하고 싶다면 {new Date().toLocaleDateString()}처럼 작성할 수 있습니다. 이것이 React의 강력한 점입니다. 마크업과 로직을 자연스럽게 결합할 수 있습니다.

CSS 스타일링은 여러 방법이 있지만, 가장 간단한 방법은 CSS 모듈을 사용하는 것입니다. page.module.css 파일을 만들고 스타일을 정의한 후, page.js에서 import styles from ‘./page.module.css’로 불러옵니다. 그리고 className={styles.container}처럼 스타일을 적용합니다. 일반 HTML에서는 class 속성을 사용하지만, JSX에서는 className을 사용한다는 점을 기억하세요.

State와 Props: React의 데이터 흐름 이해하기

React 애플리케이션을 만들 때 가장 중요한 개념이 state와 props입니다. 이 둘은 컴포넌트가 데이터를 다루는 두 가지 방식입니다.

State는 컴포넌트 내부에서 관리되는 데이터입니다. 시간이 지나면서 변할 수 있는 값들을 state로 관리합니다. 예를 들어 카운터 버튼의 숫자, 입력 폼의 내용, 토글 스위치의 on/off 상태 등이 state입니다.

React에서 state를 사용하려면 useState라는 훅을 사용합니다. 훅은 React 16.8 버전에서 도입된 기능으로, 함수 컴포넌트에서도 상태와 생명주기 기능을 사용할 수 있게 해줍니다. useState를 호출하면 현재 상태값과 그것을 업데이트하는 함수를 반환합니다.

간단한 카운터를 만들어보면서 이해해봅시다. useState(0)을 호출하면 초기값이 0인 state를 만들고, 배열 구조 분해를 통해 count와 setCount를 받습니다. count는 현재 값이고 setCount는 값을 변경하는 함수입니다. 버튼을 클릭하면 setCount(count + 1)을 호출해서 값을 증가시킵니다.

중요한 원칙이 있습니다. state를 직접 수정하면 안 됩니다. count = count + 1처럼 직접 할당하면 React가 변경을 감지하지 못해 화면이 업데이트되지 않습니다. 반드시 setState 함수를 사용해야 합니다. 이는 React가 변경을 추적하고 필요한 컴포넌트만 다시 렌더링하기 위함입니다.

Props는 부모 컴포넌트에서 자식 컴포넌트로 전달되는 데이터입니다. HTML 속성처럼 작동하지만 어떤 JavaScript 값이든 전달할 수 있습니다. 문자열, 숫자, 객체, 배열, 심지어 함수도 전달할 수 있습니다.

예를 들어 재사용 가능한 Button 컴포넌트를 만든다고 해봅시다. 이 버튼은 여러 곳에서 다른 텍스트와 동작으로 사용됩니다. Button 컴포넌트는 props로 text와 onClick을 받아서 사용합니다. 부모 컴포넌트에서는 Button text=”클릭하세요” onClick={handleClick}처럼 props를 전달합니다.

Props는 읽기 전용입니다. 자식 컴포넌트는 받은 props를 변경할 수 없습니다. 데이터는 항상 위에서 아래로 흐릅니다. 이를 단방향 데이터 흐름이라고 하며, 애플리케이션의 데이터 흐름을 예측 가능하게 만듭니다.

실용적인 예제: 할 일 목록 애플리케이션 만들기

이론을 배웠으니 실제 동작하는 애플리케이션을 만들어봅시다. 할 일 목록은 state 관리, 이벤트 처리, 리스트 렌더링 등 React의 핵심 개념을 모두 연습할 수 있는 좋은 예제입니다.

먼저 필요한 state를 생각해봅시다. 할 일 목록 배열과 새로운 할 일을 입력받을 텍스트 필드가 필요합니다. useState를 두 번 사용해서 todos 배열과 inputValue 문자열을 관리합니다. 초기값으로 todos는 빈 배열, inputValue는 빈 문자열을 줍니다.

입력 필드는 제어 컴포넌트로 만듭니다. 제어 컴포넌트란 React state와 동기화된 입력 요소를 말합니다. input 태그의 value를 inputValue로 설정하고, onChange 이벤트에서 setInputValue를 호출합니다. 이렇게 하면 사용자가 타이핑할 때마다 state가 업데이트되고, state가 변경되면 input의 값도 업데이트됩니다.

할 일 추가 기능을 구현해봅시다. 버튼을 클릭하면 addTodo 함수가 실행됩니다. 이 함수는 먼저 입력값이 비어있는지 확인합니다. 비어있다면 아무것도 하지 않고 return합니다. 입력값이 있다면 새로운 할 일 객체를 만듭니다. 각 할 일은 고유한 id, 텍스트 내용, 완료 여부를 가집니다.

중요한 점은 기존 배열을 직접 수정하지 않고 새로운 배열을 만들어야 한다는 것입니다. spread 연산자를 사용해서 기존 todos 배열의 모든 항목을 복사하고, 새로운 항목을 추가합니다. setTodos([…todos, newTodo])처럼 작성합니다. 이것이 불변성 원칙입니다. React는 참조가 변경되었을 때 리렌더링을 트리거하므로 새로운 배열을 만들어야 합니다.

할 일 목록을 화면에 표시하려면 map 함수를 사용합니다. todos.map()은 배열의 각 항목을 JSX로 변환합니다. 리스트를 렌더링할 때는 각 항목에 고유한 key prop을 제공해야 합니다. key는 React가 어떤 항목이 변경되었는지 효율적으로 파악하는 데 사용됩니다. 일반적으로 데이터베이스 ID나 고유한 식별자를 사용합니다.

삭제 기능도 추가해봅시다. 각 할 일 항목 옆에 삭제 버튼을 넣고, 클릭하면 해당 항목을 제거합니다. filter 메서드를 사용하면 특정 id를 가진 항목을 제외한 새 배열을 만들 수 있습니다. setTodos(todos.filter(todo => todo.id !== id))처럼 작성합니다.

완료 토글 기능은 조금 더 복잡합니다. map을 사용해서 배열을 순회하면서, 클릭된 항목만 completed 값을 반전시킵니다. 나머지 항목은 그대로 유지합니다. 이렇게 배열의 특정 항목만 업데이트하는 패턴은 React에서 매우 자주 사용됩니다.

데이터 페칭과 서버 컴포넌트

실제 애플리케이션은 외부 API에서 데이터를 가져와야 하는 경우가 많습니다. Next.js 13 이후 버전에서는 서버 컴포넌트라는 새로운 개념이 도입되었습니다. 서버 컴포넌트는 서버에서만 실행되며, 클라이언트로 JavaScript를 전송하지 않습니다.

기본적으로 app 폴더의 모든 컴포넌트는 서버 컴포넌트입니다. 서버 컴포넌트에서는 async/await를 직접 사용할 수 있습니다. 예를 들어 블로그 게시물 목록을 API에서 가져온다면, 컴포넌트 함수를 async로 만들고 fetch를 await합니다.

fetch는 브라우저와 Node.js에서 모두 사용할 수 있는 표준 API입니다. Next.js는 fetch를 확장해서 자동 캐싱과 재검증 기능을 제공합니다. fetch 옵션에 next 객체를 추가해서 캐시 동작을 제어할 수 있습니다.

예를 들어 { next: { revalidate: 3600 } }을 옵션으로 주면, 데이터를 3600초 동안 캐시하고 그 이후에는 재검증합니다. 이는 정적 사이트의 속도와 동적 콘텐츠의 유연성을 모두 얻을 수 있게 해줍니다.

클라이언트 컴포넌트가 필요한 경우도 있습니다. useState나 useEffect 같은 훅을 사용하거나, 브라우저 이벤트를 처리하거나, 브라우저 전용 API를 사용할 때는 클라이언트 컴포넌트여야 합니다. 파일 맨 위에 ‘use client’ 지시어를 추가하면 해당 컴포넌트가 클라이언트 컴포넌트가 됩니다.

서버와 클라이언트 컴포넌트를 적절히 조합하는 것이 중요합니다. 일반적인 패턴은 레이아웃과 데이터 페칭은 서버 컴포넌트로 하고, 인터랙티브한 UI 요소만 클라이언트 컴포넌트로 만드는 것입니다. 이렇게 하면 클라이언트로 전송되는 JavaScript 양을 최소화할 수 있습니다.

라우팅과 네비게이션

웹 애플리케이션은 여러 페이지로 구성됩니다. Next.js의 파일 시스템 기반 라우팅을 이해하면 복잡한 애플리케이션 구조도 쉽게 만들 수 있습니다.

app 폴더의 구조가 곧 URL 구조입니다. app/about/page.js는 /about 경로가 되고, app/blog/page.js는 /blog 경로가 됩니다. 중첩된 폴더를 만들면 중첩된 경로가 됩니다. app/blog/[slug]/page.js처럼 대괄호로 감싼 이름은 동적 세그먼트가 됩니다.

동적 라우트는 매우 유용합니다. 블로그 게시물이 수백 개 있다면 각각을 위해 파일을 만들 수 없습니다. 대신 [slug]처럼 동적 세그먼트를 만들고, 컴포넌트에서 params를 통해 실제 값을 받습니다. /blog/first-post로 접근하면 params.slug가 ‘first-post’가 됩니다.

페이지 간 이동은 Link 컴포넌트를 사용합니다. 일반 a 태그를 사용하면 전체 페이지가 새로고침되지만, Link는 클라이언트 사이드 네비게이션을 수행합니다. 필요한 부분만 업데이트되어 더 빠르고 부드러운 경험을 제공합니다.

프로그래매틱 네비게이션이 필요할 때는 useRouter 훅을 사용합니다. 예를 들어 폼 제출 후 다른 페이지로 이동하거나, 특정 조건에서 리다이렉트해야 할 때 router.push()를 호출합니다. useRouter는 클라이언트 컴포넌트에서만 사용할 수 있다는 점을 기억하세요.

성능 최적화 기법

애플리케이션이 커지면 성능이 중요해집니다. Next.js는 많은 최적화를 자동으로 해주지만, 개발자가 알아야 할 기법들도 있습니다.

이미지 최적화는 웹 성능에서 가장 중요한 부분 중 하나입니다. Next.js의 Image 컴포넌트를 사용하면 자동으로 이미지를 최적화합니다. 적절한 크기로 리사이징하고, 현대적인 포맷으로 변환하고, 지연 로딩을 적용합니다. 일반 img 태그 대신 Image를 사용하는 것만으로 큰 성능 향상을 얻을 수 있습니다.

코드 분할은 자동으로 이루어집니다. 각 페이지는 독립적인 번들로 분리되어, 사용자는 현재 보고 있는 페이지에 필요한 코드만 다운로드합니다. dynamic import를 사용하면 컴포넌트 수준에서도 지연 로딩을 적용할 수 있습니다. 초기 로딩 시에는 필요 없는 무거운 컴포넌트를 나중에 로드하도록 할 수 있습니다.

React의 memo를 사용하면 불필요한 리렌더링을 방지할 수 있습니다. 부모 컴포넌트가 리렌더링될 때 자식 컴포넌트도 기본적으로 리렌더링됩니다. 하지만 자식의 props가 변경되지 않았다면 굳이 리렌더링할 필요가 없습니다. memo로 컴포넌트를 감싸면 props가 변경되었을 때만 리렌더링됩니다.

배포와 실제 운영

개발이 완료되면 애플리케이션을 배포해야 합니다. Next.js 애플리케이션을 배포하는 가장 쉬운 방법은 Vercel을 사용하는 것입니다. Vercel은 Next.js를 만든 회사에서 운영하는 플랫폼으로 최적화되어 있습니다.

GitHub 저장소와 연결하면 자동으로 배포됩니다. 새로운 커밋을 푸시할 때마다 자동으로 빌드되고 배포됩니다. 프리뷰 배포 기능도 있어서 각 pull request마다 독립적인 미리보기 URL을 받을 수 있습니다.

환경 변수 관리도 중요합니다. API 키나 데이터베이스 연결 정보 같은 민감한 정보는 코드에 직접 넣으면 안 됩니다. .env.local 파일에 환경 변수를 정의하고, process.env로 접근합니다. Vercel 대시보드에서 프로덕션 환경 변수를 설정할 수 있습니다.

마치며: 지속적인 학습을 위한 조언

React와 Next.js는 강력하지만 학습 곡선이 있는 기술입니다. 처음에는 개념이 많아서 압도적으로 느껴질 수 있습니다. 하지만 작은 프로젝트부터 시작해서 점진적으로 기능을 추가하다 보면 자연스럽게 익숙해집니다.

공식 문서는 최고의 학습 자료입니다. React 공식 문서는 최근 완전히 새로 작성되었고, 인터랙티브한 예제와 상세한 설명이 포함되어 있습니다. Next.js 문서도 매우 잘 정리되어 있으며, 다양한 예제 프로젝트를 제공합니다.

실제 프로젝트를 만들어보는 것이 가장 효과적인 학습 방법입니다. 개인 블로그, 포트폴리오 사이트, 간단한 생산성 도구 등 자신에게 필요한 것을 만들어보세요. 문제를 해결하는 과정에서 깊이 있는 이해가 생깁니다.

커뮤니티도 활발합니다. Stack Overflow, Reddit의 r/reactjs, Next.js Discord 서버 등에서 질문하고 다른 개발자들과 교류할 수 있습니다. 오픈 소스 프로젝트에 기여하는 것도 좋은 학습 방법입니다. 다른 사람의 코드를 읽고 이해하는 과정에서 많은 것을 배울 수 있습니다.

웹 개발 생태계는 빠르게 변화합니다. 새로운 기능과 패턴이 계속 나옵니다. 하지만 핵심 개념은 안정적입니다. 컴포넌트, state, props, 단방향 데이터 흐름 같은 기본 원칙을 확실히 이해하면 새로운 변화에도 쉽게 적응할 수 있습니다.

이 글이 React와 Next.js를 시작하는 데 도움이 되었기를 바랍니다. 웹 개발은 창의성과 문제 해결 능력을 발휘할 수 있는 흥미로운 분야입니다. 지금 당장 첫 프로젝트를 시작해보세요. 작은 시작이 큰 성과로 이어집니다.


댓글 남기기