본문 바로가기
개발/웹 개발

Day 22-23: 클라이언트 사이드 라우팅과 전역 상태 관리

1. 오늘의 학습 목표

학습 목표: 이번 통합 세션에서는 두 가지 핵심 주제를 다룹니다. 먼저, 여러 페이지로 구성된 단일 페이지 애플리케이션(SPA)을 만들기 위한 클라이언트 사이드 라우팅의 개념을 이해합니다. 이어서, 여러 컴포넌트가 공유하는 데이터를 효율적으로 관리하기 위한 전역 상태 관리(Global State Management)와 React의 Context API 사용법을 배웁니다.

 

핵심 요약: 먼저 라우팅으로 앱의 구조를 잡고, 다음으로 Context API를 이용해 컴포넌트 간 데이터 흐름을 원활하게 만들어 복잡한 애플리케이션의 기반을 다집니다.


Part 1. 클라이언트 사이드 라우팅

2. 핵심 개념 파헤치기

2.1. 클라이언트 사이드 라우팅이란?

전통적인 웹사이트는 링크를 클릭할 때마다 서버에 새로운 페이지를 요청하고, 전체 페이지를 새로고침하여 받아옵니다. 반면 클라이언트 사이드 라우팅(CSR)은 최초에 필요한 리소스를 모두 받아온 뒤, 페이지 이동이 필요할 때 서버에 추가 요청 없이 JavaScript가 UI를 동적으로 변경하여 마치 페이지가 이동하는 것처럼 보여주는 기술입니다. 이렇게 만들어진 웹을 단일 페이지 애플리케이션(Single Page Application, SPA)이라고 합니다

장점: 페이지 새로고침이 없어 사용자 경험이 부드럽고 빠릅니다

작동 원리

  1. 사용자가 링크를 클릭하면 브라우저의 기본 동작(새로고침)을 막습니다.
  2. JavaScript가 history.pushState() API 등을 사용해 브라우저의 주소창 URL을 변경합니다
  3. 변경된 URL에 맞는 컴포넌트를 화면에 렌더링합니다

3. 코드로 배우기

3.1. 구현 목표

React Router와 같은 라이브러리 없이, useState를 이용해 클라이언트 사이드 라우팅의 기본 원리를 흉내 내 봅니다. Home과 About 페이지를 전환하는 간단한 네비게이션을 만듭니다.

 

3.2. [1단계: useState로 현재 페이지 상태 관리하기]

HomePage와 AboutPage 컴포넌트를 정의하고, useState를 이용해 어떤 페이지를 보여줄지를 page라는 상태로 관리합니다. 버튼 클릭 시 setPage를 호출하여 이 상태를 변경합니다.

const HomePage = () => <h2>Home Page</h2>;
const AboutPage = () => <h2>About Page</h2>;

function SimpleRouter() {
  const [page, setPage] = React.useState('home');

  return (
    <div>
      <h2>Part 1: 라우팅 예제</h2>
      <nav>
        <button onClick={() => setPage('home')}>Home</button>
        <button onClick={() => setPage('about')}>About</button>
      </nav>
      {/* 조건부 렌더링 */}
      {page === 'home' && <HomePage />}
      {page === 'about' && <AboutPage />}
    </div>
  );
}


Part 2. 전역 상태 관리 

4. 핵심 개념 파헤치기

4.1. 왜 전역 상태 관리가 필요한가? - Prop Drilling 문제

React의 데이터 흐름은 기본적으로 부모에서 자식으로, Props를 통해 단방향으로 전달됩니다. 하지만 앱이 복잡해져 여러 단계로 깊숙이 중첩된 컴포넌트에게 데이터를 전달해야 할 경우, 중간에 있는 모든 컴포넌트들을 거쳐 Props를 전달해야 하는 "Prop Drilling" 현상이 발생합니다. 이는 코드를 번거롭게 하고 유지보수를 어렵게 만듭니다.

 

4.2. Context API란?

Context API는 Prop Drilling 문제 없이, React 컴포넌트 트리 전체에 걸쳐 데이터를 공유할 수 있게 해주는 기능입니다. 마치 애플리케이션의 모든 컴포넌트가 구독할 수 있는 '전역 방송 채널'을 만드는 것과 같습니다.

  • 작동 원리
    1. React.createContext(): 데이터를 담을 Context 객체를 생성합니다.
    2. <Context.Provider value={...}>: Context를 구독하는 모든 하위 컴포넌트에게 value로 전달한 데이터를 제공합니다.
    3. React.useContext(Context): 하위 컴포넌트에서 Provider가 제공한 value를 읽어오는 Hook입니다.

5. 코드로 배우기

5.1. 구현 목표

Prop Drilling 없이, 상위 컴포넌트의 '테마(다크/라이트 모드)' 상태를 여러 하위 컴포넌트에서 직접 접근하고 변경할 수 있는 기능을 Context API로 구현합니다.

 

5.2. [1단계: Context 생성 및 Provider 컴포넌트 만들기]

테마 정보를 담을 ThemeContext를 만들고, 이 Context의 Provider를 사용하여 하위 컴포넌트들에게 theme 상태와 toggleTheme 함수를 제공하는 ThemeProvider 컴포넌트를 만듭니다.

//1. Context 생성
const ThemeContext = React.createContext();

  //2. Provider 컴포넌트 생성
function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState('light');
  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  const value = { theme, toggleTheme };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

 

5.3. [2단계: 하위 컴포넌트에서 useContext로 상태 사용하기]

useContext Hook을 사용하여 ThemeProvider가 제공하는 theme과 toggleTheme 함수를 직접 가져와 사용합니다.

function ThemedButton() {
  	//3. useContext로 데이터 사용
  const { theme, toggleTheme } = React.useContext(ThemeContext);

  const style = {
    backgroundColor: theme === 'light' ? '#fff' : '#333',
    color: theme === 'light' ? '#000' : '#fff',
  };

  return (
    <button onClick={toggleTheme} style={style}>
      Toggle Theme (Current: {theme})
    </button>
  );
}

6. 전체 코드

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>React Routing & Context</title>
  <script crossorigin src="[https://unpkg.com/react@17/umd/react.development.js](https://unpkg.com/react@17/umd/react.development.js)"></script>
  <script crossorigin src="[https://unpkg.com/react-dom@17/umd/react-dom.development.js](https://unpkg.com/react-dom@17/umd/react-dom.development.js)"></script>
  <script src="[https://unpkg.com/@babel/standalone/babel.min.js](https://unpkg.com/@babel/standalone/babel.min.js)"></script>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    div { margin-bottom: 20px; } button { margin-right: 10px; }
  </style>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
   //Part 1. 라우팅 예제 컴포넌트
    const HomePage = () => <p>Welcome to the Home Page!</p>;
    const AboutPage = () => <p>This is the About Page.</p>;
    function SimpleRouter() {
      const [page, setPage] = React.useState('home');
      return (
        <div>
          <h2>Part 1: 클라이언트 사이드 라우팅 흉내내기</h2>
          <nav>
            <button onClick={() => setPage('home')}>Home</button>
            <button onClick={() => setPage('about')}>About</button>
          </nav>
          {page === 'home' ? <HomePage /> : <AboutPage />}
        </div>
      );
    }

  //Part 2. Context API 예제 컴포넌트
    const ThemeContext = React.createContext();

    function ThemeProvider({ children }) {
      const [theme, setTheme] = React.useState('light');
      const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
      const value = { theme, toggleTheme };
      return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
    }

    function ThemedButton() {
      const { theme, toggleTheme } = React.useContext(ThemeContext);
      const style = {
        padding: '10px',
        borderRadius: '5px',
        border: '1px solid',
        backgroundColor: theme === 'light' ? '#eee' : '#555',
        color: theme === 'light' ? '#000' : '#fff',
      };
      return <button onClick={toggleTheme} style={style}>Toggle Theme</button>;
    }
    
    function ContextExample() {
      return (
        <ThemeProvider>
          <h2>Part 2: 전역 상태 관리 (Context API)</h2>
          <ThemedButton />
        </ThemeProvider>
      );
    }

  //App 컴포넌트에서 두 예제를 모두 렌더링
    function App() {
      return (
        <div>
          <h1>Day 22-23: Client-Side Routing & Global State</h1>
          <hr />
          <SimpleRouter />
          <hr />
          <ContextExample />
        </div>
      );
    }

    ReactDOM.render(<App />, document.getElementById('root'));
  </script>
</body>
</html>