면접관이 React Router 동작 원리를 물어본다면? History API로 답변하세요 🎯

타락한스벨트전도사·2024년 10월 29일
61
post-thumbnail

"Router의 동작 원리를 설명해주시겠어요?"

이 질문에 식은땀을 흘리신 적 있나요? 😰

면접장에서 React Router의 동작 원리를 물어보면 대부분의 개발자들이 이렇게 답변합니다:
"SPA에서 페이지를 새로고침하지 않고 이동하기 위해 사용합니다."

면접관의 표정이 미묘하게 변하기 시작합니다.
틀린 말은 아니지만... 뭔가 부족하다는 걸 느끼시나요?

실제로 이런 기본적인 답변으로는 면접관의 기대를 충족시키기 어렵습니다. 면접관은 여러분이 라이브러리를 '사용'하는 것을 넘어, 그 동작 원리까지 이해하고 있는지 확인하고 싶어 합니다.

오늘은 제가 History API를 활용해 면접관을 웃음짓게 만들 답변을 준비해보겠습니다. 🎯

면접관과 함께하는 Router 구현 여행 🤔

Q: "React Router를 직접 구현해보실 수 있을까요?"

A: (식은땀을 흘리며) "Router... 구현이요...? 음... 아직 구체적으로 어떻게 구현해야 할지 감이 안 잡히네요..."

Q: "괜찮습니다. 천천히 생각해보죠. Link 컴포넌트부터 시작해볼까요? 새로고침 없이 페이지를 전환하려면 어떻게 해야 할까요?"

A: "음, 일단 a 태그의 기본 동작을 막아야 할 것 같아요. preventDefault()를 사용하면 새로고침은 막을 수 있을 것 같습니다."

Q: "좋습니다. 그런데 실제 Link 컴포넌트를 보면 URL은 변하더라구요?"

A: (고민하며) "아... 그러네요. URL을 변경하려면... 혹시 a 태그에서 보내는 네트워크 요청을 가로채는 API가 있을까요?"

Q: "History API라고 들어보셨나요?"

A: "아니요, 처음 들어보는데요?"

Q: "브라우저에서 제공하는 API인데요, pushState라는 메서드를 사용하면 페이지 새로고침 없이 URL만 변경할 수 있습니다."

A: "아하! 그러면 Link 컴포넌트는 preventDefault로 새로고침을 막고, pushState로 URL만 바꾸는 거군요! 근데 URL이 바뀌면 Router는 어떻게 알 수 있을까요?"

Q: "좋은 질문입니다. pushState가 호출될 때 어떤 일이 일어날까요?"

A: "음... pushState가 호출되면 URL이 변경되니까, 이때 Router에게 알려줘야 할 것 같아요. 아마도 이벤트를 발생시켜야 하지 않을까요?"

Q: "네, 맞습니다. 그런데 브라우저 뒤로가기 버튼을 눌렀을 때는 어떻게 할까요?"

A: "음... 이건 브라우저에서 따로 이벤트를 제공해줘야 할 것 같은데..."

Q: "네, popstate라는 이벤트가 있습니다."

A: "아하! 이제 전체 그림이 그려지는 것 같아요. Router는 두 가지 이벤트를 구독하면 되겠네요. pushState 호출 시 발생시키는 커스텀 이벤트와 브라우저의 popstate 이벤트요. 이 이벤트들이 발생하면 현재 URL에 맞는 컴포넌트를 보여주면 되겠죠?"

Q: "정확합니다! 이제 Router를 구현하실 수 있겠나요?"

A: "네! 한번 해볼게요!"


여담: 이런 식으로 부족한 지식을 면접관이 하나씩 제공해가며 지원자의 문제해결 능력을 유도하는 면접법은 제가 당근 프론트엔드 코어팀 면접을 볼 때 경험했던 방식입니다. 단순히 알고 있는 지식을 확인하는 것이 아니라, 주어진 정보를 바탕으로 문제를 해결해나가는 과정을 보는 거죠. 나중에 제가 면접관이 된다면 이런 방식을 꼭 써먹어보고 싶네요! 😊

웹의 진화와 History API의 등장 🌟

웹은 본래 문서들의 하이퍼링크로 연결된 구조였습니다. 링크를 클릭하면 새로운 문서를 서버에서 받아오고, 브라우저는 처음부터 다시 렌더링을 시작하죠. 하지만 웹이 점점 애플리케이션화 되면서, 이런 방식은 한계에 부딪혔습니다.

그래서 HTML5에서는 History API가 도입되었습니다. 이제 개발자는 브라우저의 주소는 바꾸면서도 새로고침은 막을 수 있게 되었죠!

History Stack의 동작 방식 🎯

1. 초기 상태
---------------
|    /home    | <- 현재
---------------

2. pushState('/about') 호출
---------------
|   /about    | <- 현재
---------------
|    /home    |
---------------

3. pushState('/contact') 호출
---------------
|  /contact   | <- 현재
---------------
|   /about    |
---------------
|    /home    |
---------------

4. 뒤로가기 클릭 (popstate 발생)
---------------
|  /contact   |
---------------
|   /about    | <- 현재
---------------
|    /home    |
---------------

실제로는 이렇게 동작합니다! 🎮

  1. Link 클릭 시:

    • 브라우저의 기본 동작을 막습니다 (preventDefault)
    • pushState로 URL만 살포시 변경
    • 커스텀 이벤트로 Router에게 "URL이 변했어요!" 알림
  2. 뒤로가기/앞으로가기 클릭 시:

    • 브라우저가 자동으로 URL 변경
    • popstate 이벤트 발생
    • Router가 이를 감지하고 컴포넌트 교체

이렇게 History API를 활용하면, 마치 여러 페이지로 구성된 것처럼 보이지만 실제로는 단일 페이지에서 모든 것이 일어나는 마법 같은 경험을 만들 수 있습니다! ✨

자, 이제 Router를 한번 구현해볼까요? 💪

앞선 대화를 통해 Router 구현에 필요한 핵심 요소들을 파악했습니다. 이를 바탕으로 실제 구현 코드를 작성해보겠습니다.

import React, { useState, useEffect, ReactNode, ReactElement, ComponentType } from 'react';

type LinkProps = {
  to: string;
  children: ReactNode;
};

const Link: React.FC<LinkProps> = ({ to, children }) => {
  const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
    e.preventDefault();

    // Update URL and dispatch custom event
    window.history.pushState({}, '', to);
    window.dispatchEvent(new Event('pushstate'));
  };

  return (
    <a href={to} onClick={handleClick}>
      {children}
    </a>
  );
};

type RouteProps = {
  path: string;
  component: ComponentType<any>;
};

const Route: React.FC<RouteProps> = ({ component: Component }) => {
  return <Component />;
};

type RouterProps = {
  children: ReactNode;
};

const Router: React.FC<RouterProps> = ({ children }) => {
  const [currentPath, setCurrentPath] = useState<string>(window.location.pathname);

  useEffect(() => {
    const handleLocationChange = () => {
      setCurrentPath(window.location.pathname);
    };

    // Register event listeners
    window.addEventListener('pushstate', handleLocationChange);
    window.addEventListener('popstate', handleLocationChange);

    return () => {
      window.removeEventListener('pushstate', handleLocationChange);
      window.removeEventListener('popstate', handleLocationChange);
    };
  }, []);

  // Convert children to an array of ReactElements
  const routes = React.Children.toArray(children) as ReactElement<RouteProps>[];

  // Find the route that matches the current path
  const activeRoute = routes.find((child) => child.props.path === currentPath);

  // Render the matched route component
  return activeRoute ? React.cloneElement(activeRoute) : null;
};

이렇게 구현된 Router는 다음과 같이 사용할 수 있습니다:

const Home: React.FC = () => {
  return <h1>Home Page</h1>;
};

const About: React.FC = () => {
  return <h1>About Page</h1>;
};

const App: React.FC = () => {
  return (
    <>
      <nav>
        <Link to="/home">Home</Link> | <Link to="/about">About</Link>
      </nav>

      <Router>
        <Route path="/home" component={Home} />
        <Route path="/about" component={About} />
      </Router>
    </>
  );
};

export default App;

이 구현의 핵심 포인트들을 살펴보면:

  1. Link 컴포넌트

    • preventDefault()로 새로고침 방지
    • pushState()로 URL 변경
    • 커스텀 이벤트로 URL 변경 알림
  2. Router 컴포넌트

    • pushstate 이벤트로 pushState 감지
    • popstate 이벤트로 브라우저 네비게이션 감지
    • URL에 따른 컴포넌트 렌더링

물론 실제 react-router는 이보다 훨씬 복잡한 기능들을 제공하지만, 이것이 바로 라우팅의 핵심 동작 원리입니다! 🎯

profile
스벨트쓰고요. 오픈소스 운영합니다

2개의 댓글

comment-user-thumbnail
2024년 10월 29일

쉽게 이해할 수 있었던 것 같네요👍👍👍

좋은 글 감사합니다🙇‍♂️

1개의 답글