웹 프로그래밍

정적 웹 페이지(Static Web Page)

2025-12-08

정적 웹 페이지와 Next.js

정적 웹 페이지(static web page)는 웹 서버에 저장된 파일이 사용자의 요청에 따라 그대로 전달되는 형태의 웹 페이지입니다. 모든 방문자에게 동일한 정보를 보여주며, 일반적으로 HTML, CSS, JavaScript 등으로 작성되어 브라우저에서 실행됩니다.

정적 웹 페이지

  • 서버에 저장된 HTML 문서가 바로 클라이언트에게 전달되며, 내용은 서버에 저장된 당시의 데이터와 동일
  • 별도의 데이터베이스나 서버 애플리케이션에서 동적으로 생성되는 과정 없이, 파일 시스템에 존재하는 HTML 파일을 그대로 보여줌
  • 모든 사용자, 모든 상황에서 동일한 내용을 제공함

정적 웹 페이지 특징

  • 업데이트가 거의 필요 없는 정보(회사 소개, 메뉴판, 백과사전 등)에 적합 \(\to\) 사용자가 특정 URL을 요청하면 바로 해당 페이지가 그대로 제공됨(회사소개 페이지, 음식점 메뉴판, 위키백과 등)
  • 정적 사이트 생성기(Next.js의 SSG, Remix, Gatsby, Hugo 등)를 활용하면, 소스코드로부터 HTML 파일을 자동으로 대량 생성할 수 있음
  • 동적 기능(Javascript, 버튼, 애니메이션 등)을 추가할 수 있으나, 기본적으로 서버에서 전달되는 HTML 파일 자체는 변하지 않음

장점과 단점

  • 빠른 로딩 속도와 우수한 보안성을 가짐 \(\to\) 서버에서 렌더링이나 데이터베이스 조회 과정이 없기 때문에 해킹 위험이 적고 트래픽이 많아도 서버 부하가 적음
  • 유지보수가 쉬우며, 비용이 저렴함
  • 데이터 변화가 많은 환경(실시간 정보, 최신 뉴스 등)은 적합하지 않음

Next.js

Next.js는 복합적인 웹 애플리케이션 개발의 문제를 해결하기 위해 2016년 등장한 React 기반 프레임워크로, 현대 프론트엔드 개발 흐름을 크게 바꾼 프로젝트입니다.

  • Next.js의 특징
    • Next.js는 빌드 시간에 미리 페이지를 생성(Static Site Generation, SSG)하는 기능이 매우 강력함 \(\to\) 블로그, 뉴스, 마케팅 랜딩페이지처럼 변동이 적은 콘텐츠를 빌드시점에 HTML로 만들어두고, CDN을 활용해 초고속 배포가 가능함
    • “next build” 명령 시 모든 페이지가 미리 렌더링 되어, 사용자가 접속할 때 서버 로드 없이 즉각적으로 HTML을 받을 수 있음
    • SSG 외에도 SSR(Server Side Rendering, 서버사이드 렌더링), CSR(Client Side Rendering, 클라이언트 사이드 렌더링), ISR(Incremental Static Regeneration, 증분 정적 생성) 등 다양한 렌더링 옵션을 상황에 따라 혼용할 수 있어, 프로젝트 요구에 따라 유연한 구성이 가능함

Remix - 데이터 우선, 서버 중심 렌더링

  • Remix는 SSG를 지원하지 않고, 기본적으로 SSR(서버사이드 렌더링)을 바탕으로 최신 데이터를 매 요청마다 로딩
  • 각 페이지 마다 필요한 데이터를 서버에서 즉시 가져와 페이지를 렌더함
  • Remix의 특징은 브라우저 캐싱, HTTP 캐시 제어, 에지(Edge) 컴퓨팅 활용 등 실제 웹에서의 최신성·응답 속도를 극대화하는 데 있음 \(\to\) 정적 파일로 미리 만들어두지 않고, 사용자가 페이지를 볼 때마다 최신 서버 데이터를 가져온 후에, SSR을 통해 렌더링하여 반환함
  • 전체 사이트를 자주 업데이트하거나, 유저별 맞춤 데이터가 중요한 서비스(예: 게시판, 대시보드)에 강점이 있음 참고

Next.js가 만들어진 배경과 이유

  • 리액트(React.js)는 SPA(Single Page Application)를 만들기에 적합했지만, SSR/SEO(Search Engine Optimization, 검색엔진 최적화)/초기 로딩 속도 등 중요한 웹 요구사항을 충족하기 어렵다는 단점이 있었음
  • CSR로만 구현된 앱은 아래와 같은 문제가 있음
    • 첫 진입시 화면이 늦게 뜸
    • 자바스크립트 비활성 환경에서 페이지 자체가 보이지 않는 문제
    • 구글, 네이버 같은 검색엔진 최적화(SEO)가 떨어지는 문제

Next.js의 철학

  • Next.js는 1) SSR을 손쉽게 도입, 2) 정적 사이트 생성(SSG), 3) 코드 스플리팅, 4) 데이터를 미리 페치하는 기능 등 “웹 서비스 품질 향상”을 위한 기능을 표준화하여, React 개발자들이 빠르고 효율적으로 서비스 규모를 확장하게 도와주려고 만들어졌음 \(to\) “복잡한 설정 없이 바로 생산성 있는 React SSR 환경을 제공한다”는 철학이 있음

역사와 발전 방향

  • 2016년: Next.js 1.0 출시, SSR과 자동 코드스플리팅(Code Splitting) 중심
  • 2017~2019년: 동적 라우팅, 서버리스 지원, 정적 사이트 생성(SSG) 및 API Routes 지원 추가
  • 2020년 이후: ISR, Image 최적화, 글로벌 CSS/모듈화, App Router 기반의 서버 컴포넌트, 파일 기반 라우팅, TypeScript 지원 추가, 다양한 환경을 지원하는 기능 추가
  • 2025년 v16: Server Actions, Turbopack, Edge Functions, React Compiler 등 “서버/클라우드 지향 최적화”가 본격 도입됨

설계 철학과 성장 비전

  • 간결함: 최소한의 설정, 폴더 중심 라우팅, 직관적 개발 경험
  • 성능: 서버 렌더링, 정적 렌더링, 코드스플리팅, 이미지/폰트 등 필수 최적화 자동화
  • 확장성: API 라우트, 미들웨어, Edge 배포와 클라우드 친화적 확장 초점
  • 개발자 경험: 핫 모듈 리플레이스먼트, 자동 새로고침 등 생산성 우선

컴포넌트

컴포넌트는 UI를 재사용 가능한 작은 단위로 나누어 관리하는 요소로, 각각의 컴포넌트가 독립적으로 화면을 구성할 수 있습니다. 이러한 컴포넌트는 입력 값(props)을 받아 그에 맞는 UI 엘리먼트를 반환하는 함수와 같은 역할을 합니다.​

함수(컴포넌트)

앞선 수많은 예제에서 소흘히 넘어갔지만, 확실한 것은 컴포넌트는 JavaScript 함수 입니다. 따라서 모든 컴포넌트는 JavaScript 함수 입니다. 우리는 TypeScript를 사용하지만, 기본적으로 TypeScript는 JavaScript를 확장한 언어이므로, 결과론적으로 JavaScript 함수라 할 수 있습니다.

// 함수 선언
function App() {
  return (
    <div>
      <h1>Hello React</h1>
    </div>
  );
}

앞선 예제를 화살표 함수 표현식으로 작성해 보겠습니다.

  • 화살표 함수(arrow function)
    • ES6(ECMAScript 2015)에서 도입된 익명 함수 표현식으로, 기존 함수보다 더 간결한 문법으로 작성할 수 있음
    • 자신만의 this 바인딩을 만들지 않고, 외부 스코프의 this 값을 그대로 사용한다는 점에서 기존 함수와 차별화
// 화살표 함수 표현식
const App = () => {
  return (
    <div>
      <h1>Hello React</h1>
    </div>
  );
};

JSX만 반환하는 경우 중괄호와 return 문을 생략할 수 있습니다. 이는 간결한 바디(concise body)라고 합니다.

// 간결한 바디 (암시적 return)
const App = () => (
  <div>
    <h1>Hello React</h1>
  </div>
);

정의와 호출(선언과 인스턴스화)

함수는 선언과 호출로 구분됩니다. 그리고 컴포넌트는 선언(declaration)과 인스턴스화(instantiation)로 구분됩니다.

// 컴포넌트 선언
function List() {
  return (
    <ul>
      <li>Item 1</li>
      <li>Item 2</li>
    </ul>
  );
}

// 컴포넌트 인스턴스화
function App() {
  return (
    <div>
      <List />  {/* 인스턴스 생성 */}
      <List />  {/* 또 다른 인스턴스 */}
    </div>
  );
}

React루 구성된 페이지는 컴포넌트 트리(component tree)로 구성됩니다.

function App() {
  return (
    <div>
      <h1>My Hacker Stories</h1>
      <Search />  {/* App의 자식 */}
      <List />    {/* App의 자식, Search의 형제 */}
    </div>
  );
}

function Search() { ... }
function List() { ... }
  • 루트 컴포넌트: 최상위 컴포넌트 (예: App)
  • 부모/자식 컴포넌트: 계층 관계
  • 형제 컴포넌트: 같은 부모를 가진 컴포넌트
<App>
├── <h1>My Hacker Stories</h1>
├── <Search />
└── <List />

매개변수(Props)

함수의 매개변수를 사용해서 외부 값을 받아 올 수 있습니다. React 컴포넌트에서는 이를 Props라고 합니다. Props는 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달하는 수단입니다.

// 부모 컴포넌트
function App() {
  const stories = [
    { title: 'React', objectID: 0 },
    { title: 'Redux', objectID: 1 },
  ];

  return (
    <div>
      <List list={stories} />  {/* props 전달 */}
    </div>
  );
}

// 자식 컴포넌트
function List(props) {
  return (
    <ul>
      {props.list.map((item) => (
        <li key={item.objectID}>{item.title}</li>
      ))}
    </ul>
  );
}

Props 객체를 구조 분해하여 더 간결하게 사용할 수 있습니다.

  • 구조 분해 할당(destructuring assignment)
    • JavaScript에서 배열이나 객체의 값을 쉽게 개별 변수에 할당할 수 있게 해주는 문법
    • 배열의 요소들을 순서대로 변수에 할당하거나, 객체의 프로퍼티를 변수명과 매칭하여 한 번에 변수에 담을 수 있어 코드가 간결하고 가독성이 높아짐
// 함수 본문에서 구조 분해
function List(props) {
  const { list } = props;
  return (
    <ul>
      {list.map((item) => (
        <li key={item.objectID}>{item.title}</li>
      ))}
    </ul>
  );
}
// 함수 시그니처에서 구조 분해 (권장)
function List({ list }) {
  return (
    <ul>
      {list.map((item) => (
        <li key={item.objectID}>{item.title}</li>
      ))}
    </ul>
  );
}
  • 읽기 전용: Props는 변경할 수 없음(immutable)
  • 단방향 데이터 흐름: 부모 \(to\) 자식으로만 전달
  • JavaScript 객체: Props는 일반 JavaScript 객체

TypeScript 컴포넌트 주의사항

컴포넌트의 props는 별도의 타입(또는 인터페이스)으로 정의해 지정합니다.

// 인터페이스/타입 선언
type HelloProps = {
  name: string;
  age?: number; // 선택적 prop
};

// 함수형 컴포넌트에서 사용
function Hello({ name, age }: HelloProps) {
  return <div>Hello, {name}! {age && `Age: ${age}`}</div>;
}
  • FC(Functional Component) 타입 명시는 권장되지 않습니다. 함수형 컴포넌트는 타입 별칭(명시적 props 타입)만으로 충분해서 React.FC(또는 React.FunctionComponent) 사용은 권장되지 않습니다.

이벤트 핸들러의 타입도 명확하게 지정합니다.

function MyButton(props: { onClick: (e: React.MouseEvent<HTMLButtonElement>) => void }) {
  return <button onClick={props.onClick}>Click</button>;
}

useRef, useState 등 훅을 사용할 때 generic 타입 매개변수로 명시적으로 타입을 넣을 수 있습니다.

const [count, setCount] = useState<number>(0);
const inputRef = useRef<HTMLInputElement>(null);

TypeScript에서는 propTypes나 defaultProps 대신 인터페이스(type)와 기본 매개변수를 활용합니다. children을 받는 컴포넌트라면 아래처럼 지정합니다.

type Props = {
  children: React.ReactNode;
}
  • 모든 props, state, 이벤트 객체 타입을 명시적으로 선언
  • any 타입 남용은 피하고, 가능한 구체적인 타입을 사용
  • JSX 반환 타입은 JSX.Element로 추론됨(명시 필요 없음)

React 컴포넌트 연습

연습(1) - app/layout.tsx 읽어보기

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}
  • RootLayout 함수는 Next.js에서 사용되는 레이아웃 컴포넌트
  • 최상단(/) 세그먼트이며, 하위 <children> 세그먼트를 인자로 받아서 화면을 구성
  • 함수의 매개변수(props)는 구조 분해 할당(destructuring)을 통해 children만 추출
  • export default를 사용, 기본 내보내기(default export) 형식으로 제공함
  • Readonly<{ children: React.ReactNode }>로 지정, Readonly<T>는 객체의 속성이 변경될 수 없음(불변 객체)
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) { ... }

children은 React에서 컴포넌트의 자식 노드를 전달할 때 사용하는 표준 속성, 타입은 React.ReactNode (= 렌더링할 수 있는 모든 React 요소, string, number, null 등) 입니다.

  • 예를 들어, <Layout>내용</Layout>처럼 Layout 컴포넌트가 있을 때, 내용 부분이 자동으로 children으로 전달
  • children을 활용하면, 여러 컴포넌트에서 동일한 레이아웃이나 래퍼 구조를 재사용할 수 있음
  • 함수형 컴포넌트에서 props의 일부로 전달되며, 보통 다음처럼 사용하며 children은 부모가 자식에게 콘텐츠를 끼워넣을 수 있게 해주는 매우 유용한 리액트의 기본 패턴임
type Props = { children: React.ReactNode };
function MyComponent({ children }: Props) { return <div>{children}</div>; }

함수(컴포넌트)는 HTML 마크업을 반환합니다.

  • 최상단에 <html lang="en"> 태그로 감싸고 있음
  • <body> 엘리먼트에 여러 클래스가 props 형태로 전달되는데, 이는 보통 폰트나 스타일 적용을 위한 것(geistSans.variable, geistMono.variable)
  • children을 그대로 body 안에서 렌더링하여, 이 레이아웃을 사용하는 페이지의 실제 콘텐츠가 해당 위치에 삽입됨

연습(2) - app/layout.tsx 읽어보기

Next.js 13+의 app 디렉터리에서 이 레이아웃 파일(app/layout.tsx)은 앱 전체 혹은 특정 라우트 그룹에 공통으로 적용되는 레이아웃 역할을 합니다.

  • 레이아웃에서 자식 요소(children)를 받아 지정한 HTML 구조에 끼워 넣는 방식으로, 모든 하위 페이지에 일관된 구조를 제공함
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
  <main>
    //...
  </main>
</div>
  • flex: flexbox 레이아웃을 활성화, 자식 요소들이 플렉스 컨테이너 안에서 정렬, 배치, 크기 조정이 유연하게 동작함
    • items-center: flexbox에서 자식 요소들을 수직(교차축) 중앙 정렬, flex direction이 기본값(row)이므로 위아래(세로축) 중앙에 위치함
    • justify-center: flexbox에서 자식 요소들을 수평(주축) 중앙 정렬, 좌우(가로축) 기준으로 중앙에 놓임
  • min-h-screen: 컨테이너의 최소 높이(min-height)를 뷰포트 전체 높이(100vh, 즉 화면 전체 높이)로 설정
  • bg-zinc-50: 배경색을 zinc 팔레트의 50단계로 지정, 아주 연한 회색(zinc-50) 계열이며, Tailwind CSS 컬러 스케일의 하나임
  • font-sans: 글꼴을 산세리프 계열로 지정, 시스템에서 산세리프 폰트를 사용하게 되며, 깔끔하고 현대적인 느낌을 줌
  • dark:bg-black: 다크 모드가 활성화된 경우(dark 프리픽스) 배경색을 검정색(black)으로 설정, Light 모드에서는 bg-zinc-50, 다크 모드에선 bg-black이 적용되어 테마를 자연스럽게 전환할 수 있음
<main className="flex w-full max-w-3xl flex-col justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
</main>
  • flex-col: flexbox 방향을 기본 가로(row)에서 세로(column)로 변경, 자식들이 위에서 아래로 쌓임
  • justify-between: flexbox의 주 축(main axis, 여기서는 세로축)에서 자식 요소들 사이를 가능한 멀리 떨어뜨려 배치
  • w-full: 요소의 가로 너비를 부모 요소 기준으로 100%로 설정
  • max-w-3xl: 최대 너비를 Tailwind에서 지정한 3xl(즉, 약 48rem)로 제한, 너무 넓어지지 않게 함
  • py-32: 위아래(padding-top, padding-bottom)에 각각 32단계(preset, 약 8rem)의 내부 여백 추가
  • px-16: 좌우(padding-left, padding-right)에 각각 16단계(약 4rem)의 내부 여백 추가
  • bg-white: 배경색을 흰색으로 지정
  • dark:bg-black: 다크 모드에서는 배경색을 검정색으로 변경
  • sm:items-start: 작은(sm, 약 640px 이상) 화면에서는 flexbox의 교차축(여기서는 가로축) 기준으로 자식 요소들을 시작점(왼쪽) 정렬
    • sm: 40rem (640px), @media (width >= 40rem) { … }
    • md: 48rem (768px), @media (width >= 48rem) { … }
    • lg: 64rem (1024px), @media (width >= 64rem) { … }
    • xl: 80rem (1280px), @media (width >= 80rem) { … }
    • 2xl: 96rem (1536px), @media (width >= 96rem) { … }
  • 클라이언트 사이드에서 렌더링되는 <Image> 태그를 사용, 이미지를 렌더링할 때 사용
<Image
  className="dark:invert"
  src="/next.svg"
  alt="Next.js logo"
  width={100}
  height={20}
  priority
/>

참고문헌