Skip to content

Instantly share code, notes, and snippets.

@ellemedit
Last active December 18, 2021 11:02
Show Gist options
  • Save ellemedit/7a33cf5e114e5f1b2616b7239d1633de to your computer and use it in GitHub Desktop.
Save ellemedit/7a33cf5e114e5f1b2616b7239d1633de to your computer and use it in GitHub Desktop.

제 κ²½ν—˜μ΄ λ‹Ήμ‹ μ˜ next.js ν”„λ‘œμ νŠΈμ— 도움이 λ˜κ±°λ‚˜ μ˜κ°μ„ μ£Όλ©΄ μ’‹κ² μŠ΅λ‹ˆλ‹€.

μ™œ νΌμ‹œμŠ€ν„΄νŠΈ λ ˆμ΄μ•„μ›ƒμ΄ ν•„μš”ν•œκ°€μš”?

κ°€μž₯ 근본적인 μ΄μœ λŠ” νΌμ‹œμŠ€ν„΄νŠΈν•˜μ§€ μ•ŠμœΌλ©΄ λͺ¨λ“  DOM λ…Έλ“œκ°€ 파괴되고 λ‹€μ‹œ μƒμ„±λœλ‹€λŠ” 점 μž…λ‹ˆλ‹€.

  1. 이전 νŽ˜μ΄μ§€μ˜ DOM Nodeκ°€ 버렀지기 λ•Œλ¬Έμ— 항상 DOM μƒνƒœλ₯Ό μžƒμ–΄λ²„λ¦½λ‹ˆλ‹€. 예λ₯Όλ“€μ–΄ λ ˆμ΄μ•„μ›ƒμ— ν”ν•˜κ²Œ μžˆμ„ 수 μžˆλŠ” 검색창 μž…λ ₯, λ„€λΉ„κ²Œμ΄μ…˜ λ©”λ‰΄μ˜ 포컀슀 μƒνƒœκ°€ νŽ˜μ΄μ§€ μ΄λ™λ§ˆλ‹€ μ‚¬λΌμ§‘λ‹ˆλ‹€.
  2. λ‹Ήμ—°ν•˜κ²Œλ„ CSS Transition이 λΆˆκ°€λŠ₯ν•©λ‹ˆλ‹€. μ• λ‹ˆλ©”μ΄μ…˜μ΄ ν•„μš”ν•˜λ‹€λ©΄ μ „ν˜€ λ‹€λ₯Έ λ°©λ²•μœΌλ‘œ κ΅¬ν˜„ν•΄μ•Ό ν•©λ‹ˆλ‹€.

μœ„ 이유둜 인해 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ μ‚¬μš©μ„±κ³Ό 접근성이 λ–¨μ–΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

μ–΄λ–€κ²Œ νΌμ‹œμŠ€ν„΄νŠΈ ν•˜μ§€ μ•Šμ€ λ ˆμ΄μ•„μ›ƒμΈκ°€μš”?

Next.js ν”„λ‘œμ νŠΈλ₯Ό μ•„λž˜μ™€ 같이 κ΅¬μ„±ν•΄λ΄…μ‹œλ‹€.

// pages/index.js
const HomePage = () => (
  <>
    <header>
      <h1>Awesome Title</h1>
    </header>
    <nav><input placeholder="Search ..." /></nav>
    <main>
      Home Page, <Link to="/second"><a>to Second Page</a></Link>
    </main>
  </>
)
// pages/second.js
const SecondPage = () => (
  <>
    <header>
      <h1>Awesome Title</h1>
    </header>
    <nav><input placeholder="Search ..." /></nav>
    <main>
      Second Page, <Link to="/home"><a>to Home Page</a></Link>
    </main>
  </>
)

HomePage와 SecondPageλŠ” 같은 μžμ‹ μ—˜λ¦¬λ¨ΌνŠΈ header, nav, main을 κ°€μ§€μ§€λ§Œ μ„œλ‘œ λ‹€λ₯Έ μ»΄ν¬λ„ŒνŠΈμ΄κΈ° λ•Œλ¬Έμ— λ‹€λ₯Έ νŽ˜μ΄μ§€λ‘œ μ„œλ‘œ νŽ˜μ΄μ§€λ₯Ό 이동할 λ•Œ λͺ¨λ“  μžμ‹ μ—˜λ¦¬λ¨ΌνŠΈλŠ” 버렀지고 μƒˆλ‘œ μƒμ„±λ©λ‹ˆλ‹€.

μ–΄λ–»κ²Œ νΌμ‹œμŠ€ν„΄νŠΈν•œ λ ˆμ΄μ•„μ›ƒμ„ λ§Œλ“€ 수 μžˆλ‚˜μš”?

next.js κ³΅μ‹λ¬Έμ„œμ— 적힌 λŒ€λ‘œ pages/_app.js에 래퍼λ₯Ό μΆ”κ°€ν•˜λŠ” κ²ƒμœΌλ‘œ κ°€λŠ₯ν•©λ‹ˆλ‹€.

// pages/_app.js
const App = ({ Component, pageProps }) => (
  <MyLayout>
    <Component {...pageProps} />
  </MyLayout>
);

const MyLayout = ({ children }) => (
  <>
    <header>
      <h1>Puesdo GitHub</h1>
    </header>
    <nav><input placeholder="Search ..." /></nav>
    <main>
      {children}
    </main>
  </>
);
// pages/index.js
const HomePage = () =>
  <>Home Page, <Link to="/second"><a>to Second Page</a></Link></>;
// pages/second.js
const SecondPage = () =>
  <>Second Page, <Link to="/home"><a>to Home Page</a></Link></>;

μœ„ μ½”λ“œμ²˜λŸΌ μž‘μ„±ν•˜λ©΄ λ ˆμ΄μ•„μ›ƒμ˜ header, nav, main μ—˜λ¦¬λ¨ΌνŠΈκ°€ 항상 μœ μ§€λ©λ‹ˆλ‹€. ν•˜μ§€λ§Œ λͺ¨λ“  νŽ˜μ΄μ§€κ°€ λ™μΌν•œ λ ˆμ΄μ•„μ›ƒ ꡬ성을 κ°€μ‘Œλ‹€κ³  κ°€μ •ν•  수 μ—†μŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄ μ—¬λŸ¬λΆ„μ΄ κΉƒν—™κ³Ό μœ μ‚¬ν•œ ν”„λ‘œμ νŠΈλ₯Ό κ°œλ°œν•œλ‹€κ³  ν•©μ‹œλ‹€. GitHub을 처음 μ ‘μ†ν–ˆμ„ λ•Œ λ³΄μ΄λŠ” νŽ˜μ΄μ§€μ™€ Organization νŽ˜μ΄μ§€μ˜ λ ˆμ΄μ•„μ›ƒμ„ λ– μ˜¬λ €λ΄…μ‹œλ‹€. 상단 λ„€λΉ„κ²Œμ΄μ…˜μ„ μ œμ™Έν•˜λ©΄ νŽ˜μ΄μ§€μ˜ λ‚˜λ¨Έμ§€ 뢀뢄은 μΌμΉ˜ν•˜λŠ” 뢀뢄이 μ—†μŠ΅λ‹ˆλ‹€. Organization νŽ˜μ΄μ§€λŠ” Repository λͺ©λ‘ 외에도 Packages, Projects 그리고 Settings 탭이 μ‘΄μž¬ν•˜κ³  각 탭은 λ ˆμ΄μ•„μ›ƒμ„ κ³΅μœ ν•˜κ³  고유 νŽ˜μ΄μ§€λ‘œ μ‘΄μž¬ν•©λ‹ˆλ‹€. 이 μš”κ΅¬μ‚¬ν•­μ„ λ§Œμ‘±ν•˜κΈ° μœ„ν•΄ λΉ λ₯΄κ²Œ κ΅¬ν˜„ν•΄λ΄…μ‹œλ‹€.

// pages/_app.js
const App = ({ Component, pageProps }) => {
  const router = useRouter();
  const Layout = DefaultLayout;
  if (router.pathname.startsWith('/[owner]')) {
    Layout = WorkspaceLayout;
  }
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

const DefaultLayout = ({ children }) => (
  <header>
    <h1>Puesdo GitHub</h1>
  </header>
  <nav>
    <Link href="/my-workspace">
      <a>My Workspace</a>
    </Link>
  </nav>
  <main>
    {children}
  </main>
);

const OrganizationLayout = ({ children }) => (
  <>
    <header>
      <h1>Puesdo GitHub</h1>
    </header>
    <nav>
      <a>Repositories</a>
      <a>Packages</a>
      <a>Settings</a>
    </nav>
    <main>
      {children}
    </main>
  </>
);

μœ„ 예제둜 λ ˆμ΄μ•„μ›ƒμ„ 뢄리할 수 있게 λ¬μ§€λ§Œ μ„Έ κ°€μ§€ 문제점이 μžˆμŠ΅λ‹ˆλ‹€.

  1. μ–΄λ–€ λ ˆμ΄μ•„μ›ƒμ΄ λ Œλ”λ§λ˜λŠ”μ§€ νŽ˜μ΄μ§€ μ»΄ν¬λ„ŒνŠΈλ§Œ 보고 μ•Œ 수 μ—†μŠ΅λ‹ˆλ‹€. pages/_app.jsλ₯Ό 확인해야 μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€. colocation을 ν•  수 μ—†κ³ , νŽ˜μ΄μ§€λ₯Ό κ°œλ°œν•˜λŠ”λ° μ•Œμ•„μ•Ό ν•˜λŠ” λ‚΄μš©μ΄ λ§Žμ•„μ Έμ„œ μœ μ§€λ³΄μˆ˜μ„±μ΄ λ–¨μ–΄μ§‘λ‹ˆλ‹€.
  2. DefaultLayout, OrganizationLayout 두 νŽ˜μ΄μ§€μ˜ λ ˆμ΄μ•„μ›ƒμ΄ μžμ‹ μ—˜λ¦¬λ¨ΌνŠΈλ“€μ΄ μž¬μ‚¬μš©λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
  3. λ ˆμ΄μ•„μ›ƒ λ‹¨μœ„μ—μ„œ data fetching이 λΆˆκ°€λŠ₯ν•©λ‹ˆλ‹€.

λ ˆμ΄μ•„μ›ƒ μ „ν™˜μ‹œμ— 같은 μ—˜λ¦¬λ¨ΌνŠΈλ₯Ό μœ μ§€ν•˜κΈ°

νŽ˜μ΄μ§€ μ»΄ν¬λ„ŒνŠΈμ— λ ˆμ΄μ•„μ›ƒμ„ λͺ…μ‹œν•˜λ„λ‘ λ°”κΏ‰λ‹ˆλ‹€. λ‹€μŒκ³Ό 같이 λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€:

// pages/index.js
const HomePage = () => {...};
HomePage.withLayout = {
  render: (children) => (
    <>
      <header>
        <h1>Puesdo GitHub</h1>
      </header>
      <nav>
        <Link href="/my-workspace">
          <a>My Workspace</a>
        </Link>
      </nav>
      <main>
        {children}
      </main>
    </>
  )
}
// pages/[owner]/index.js
const OrganizationPage = () => {...};
OrganizationPage.withLayout = {
  render: (children) => (
    <>
      <header>
        <h1>Puesdo GitHub</h1>
      </header>
      <nav>
        <a>Repositories</a>
        <a>Packages</a>
        <a>Settings</a>
      </nav>
      <main>
        {children}
      </main>
    </>
  )
}
// pages/_app.js
const App = ({ Component, pageProps, layoutData }) => {
  const renderLayout = Component.withLayout?.render
    || (page) => page;
  return renderLayout(<Component {...pageProps} />);
};

μ΄λ ‡κ²Œ κ΅¬μ„±ν•˜λ©΄ λ ˆμ΄μ•„μ›ƒμ„ νŽ˜μ΄μ§€μ—μ„œ λͺ…μ‹œν•  수 μžˆμŠ΅λ‹ˆλ‹€. 그리고 λ ˆμ΄μ•„μ›ƒμ˜ μ—˜λ¦¬λ¨ΌνŠΈ μœ„μΉ˜κ°€ 트리 λ‚΄μ—μ„œ 항상 μΌμ •ν•˜κΈ° λ•Œλ¬Έμ— DOM λ…Έλ“œλ₯Ό μž¬μ‚¬μš©ν•˜κ²Œ λ©λ‹ˆλ‹€.

λ ˆμ΄μ•„μ›ƒμ—μ„œ ν•„μš”ν•œ 데이터λ₯Ό Fetchingν•˜κΈ°

λͺ‡λͺ‡ λ ˆμ΄μ•„μ›ƒμ€ SSR쀑에 λΉ„λ™κΈ°μ μœΌλ‘œ 데이터λ₯Ό 가져와야 ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ•„μ£Ό κ°„λ‹¨ν•˜κ²Œ getInitialProps 와 λΉ„μŠ·ν•œ 녀석을 κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

const App = ({ Component, pageProps, layoutData }) => {
  const renderLayout = Component.withLayout?.render
    || (page) => page;
  return renderLayout(<Component {...pageProps} />, layoutData);
};

App.getInitialProps = async ({ ctx, Component }) => {
  const [pageProps, layoutData] = await Promise.all([
    Component.getInitialProps?.(ctx),
    Component.withLayout?.fetchInitialData?.(ctx),
  ]);
  return { pageProps, layoutData };
}

7쀄을 μΆ”κ°€ν•˜μ—¬ μ•„λž˜μ™€ 같이 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

// pages/[owner]/index.js
const OrganizationPage = () => {...};
OrganizationPage.withLayout = {
  render: (children, { status, organization }) => status === 'stand-by' ? (
    <>
      <header>
        <h1>{organization.name}</h1>
        <p>{organization.description}</p>
      </header>
      <nav>
        <a>Repositories</a>
        <a>Packages</a>
        <a>Settings</a>
      </nav>
      <main>
        {children}
      </main>
    </>
  ) : (
    <>
      <header>
        <h1>404 Not Found</h1>
        <p>Requested organization does not exist</p>
      </header>
    </>
  ),
  fetchInitialData: async (context) => {
    const ownerId = context.query.owner;
    try {
      const organization = await fetchOrganizationByOwnerId(ownerId);
      return { organization, status: 'stand-by' };
    } catch (error) {
      if (error.response.status === 404) {
        return { status: 'not-found' };
      }
      throw error;
    }
  },
}

이제 λŒ€λΆ€λΆ„μ˜ λ ˆμ΄μ•„μ›ƒ μš”κ΅¬μ‚¬ν•­μ΄ μΆ©μ‘±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

  1. SSR 과정쀑에 Data Fetching κ°€λŠ₯
  2. νŽ˜μ΄μ§€ λ‹¨μœ„λ‘œ λ ˆμ΄μ•„μ›ƒμ„ λͺ…μ‹œν•  수 있음
  3. λ ˆμ΄μ•„μ›ƒ ν•˜μœ„ μš”μ†Œκ°€ μž¬μ‚¬μš©λ¨

μ΄κ²ƒλ§ŒμœΌλ‘œλ„ λŒ€λΆ€λΆ„μ˜ μƒν™©μ—μ„œ ν›Œλ₯­ν•˜κ²Œ νŽ˜μ΄μ§€ λ ˆμ΄μ•„μ›ƒμ„ ꡬ성할 수 μžˆμŠ΅λ‹ˆλ‹€.

λ ˆμ΄μ•„μ›ƒ 뢄리

μ—¬λŸ¬ λ ˆμ΄μ•„μ›ƒμ„ ν•©μ„±ν•  수 있으면 λ ˆμ΄μ•„μ›ƒμ„ λΆ„λ¦¬ν•˜κ³  μž¬μ‚¬μš©ν•˜κΈ° 훨씬 μ‰¬μ›Œμ§‘λ‹ˆλ‹€. λ‹€μŒκ³Ό 같은 λ ˆμ΄μ•„μ›ƒλ“€μ΄ μžˆλ‹€κ³  ν•΄λ΄…μ‹œλ‹€.

const DefaultLayout = {
  render: (children) => (
    <>
      <TopNavigation />
      <div>{children}</div>
    </>
  ),
};

const OrganizationLayout = {
  render: (children) => (
    <>
      <TopNavigation />
      <WorkspaceSideNavigation />
      {children}
    </>
  ),
};

const OrganizationProjectLayout = {
  render: (children) => (
    <>
      <TopNavigation />
      <WorkspaceSideNavigation />
      <div>
        <ProjectNavigation />
        {children}
      </div>
    </>
  ),
};

각 λ ˆμ΄μ•„μ›ƒμ΄ 이전 λ ˆμ΄μ•„μ›ƒκ³Ό ꡉμž₯히 μœ μ‚¬ν•©λ‹ˆλ‹€. OrganizationProjectLayoutλŠ” OrganizationLayoutκ°€ ν™•μž₯된 ν˜•νƒœκ³  OrganizationLayoutλŠ” DefaultLayoutκ°€ ν™•μž₯된 ν˜•νƒœμž…λ‹ˆλ‹€. μœ„ λ ˆμ΄μ•„μ›ƒμ—μ„œ μ€‘λ³΅λ˜λŠ” 뢀뢄을 λΆ„λ¦¬ν•΄μ„œ μ‚¬μš©ν•  수 있으면 λ”μš± νŽΈλ¦¬ν•  κ²ƒμž…λ‹ˆλ‹€.

const DefaultLayout = {
  render: (children) => (
    <>
      <TopNavigation />
      {children}
    </>
  ),
};

const OrganizationLayout = {
  render: (children) => (
    <>
      <WorkspaceSideNavigation />
      {children}
    </>
  ),
  parent: DefaultLayout,
};

const OrganizationProjectLayout = {
  render: (children) => (
    <div>
      <ProjectNavigation />
      {children}
    </div>
  ),
  parent: OrganizationLayout,
};

λ³΅μž‘ν•œ CSSλ‚˜ λ Œλ”λ§ 둜직이 ν¬ν•¨λ˜μ–΄ μžˆμ§€ μ•Šμ•„ 쀄어든 뢀뢄이 λΆ€κ°λ˜μ§€ μ•Šμ§€λ§Œ, 일반적으둜 문제λ₯Ό 뢄리할 수 있으면 λ‹¨μˆœν™”μ‹œμΌœμ„œ 더 μ‰½κ²Œ κ°œλ°œν•  수 μžˆλ“―μ΄ λ ˆμ΄μ•„μ›ƒλ„ λΉ„μŠ·ν•˜κ²Œ 생각할 수 μžˆμŠ΅λ‹ˆλ‹€. 각 λ ˆμ΄μ•„μ›ƒλ³„λ‘œ λ‹΄λ‹Ήν•˜λŠ” 뢀뢄은 λ‹€λ₯΄μ§€λ§Œ μ΅œμ’…μ μœΌλ‘œ OrganizationProjectLayoutκ°€ μ•„λž˜ 처럼 κ²°κ³Όλ₯Ό λ°˜ν™˜ν•˜λ©΄ λ˜λŠ”κ±°μ£ .

// μ‹€μ œλ‘  μ΄λ ‡κ²Œ λ™μž‘ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
// μš°λ¦¬κ°€ μ •μ˜ν•œ Layout듀은 Componentκ°€ μ•„λ‹ˆλ‹ˆκΉŒμš”.
<DefaultLayout>
  <OrganizationLayout>
    <OrganizationProjectLayout>
      {children}
    </OrganizationProjectLayout>
  </OrganizationLayout>
</DefaultLayout>

μ΄λ ‡κ²Œ 되면 λ ˆμ΄μ•„μ›ƒ λ‹¨κ³„λ³„λ‘œ 관심사λ₯Ό 뢄리할 수 있게 λ©λ‹ˆλ‹€. μ•„μ£Ό κ°„λ‹¨ν•˜κ²Œ κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

// pages/_app.js
const App = ({ Component, pageProps, layoutDataList }) => {
  const layouts = getLayoutList(Component.withLayout);
  return layouts.reduce(
    (children, { render }) => render(children, layoutDataList),
    <Component {...pageProps}>,
  );
};

App.getInitialProps = async ({ ctx, Component }) => {
  const [pageProps, layoutDataList] = await Promise.all([
    Component.getInitialProps?.(ctx),
    // 비동기 μ²˜λ¦¬μ— μœ μ˜ν•˜μ„Έμš”
    Promise.all(
      getLayoutList(Component).map(
        layout => layout.withLayout?.fetchInitialData?.(ctx)
      )
    ),
  ]);
  return { pageProps, layoutDataList };
}

const getLayoutList = (layout) => {
  const list = [];
  let lastLayout = layout;
  while (lastLayout) {
    list.push(layout);
    lastLayout = layout.parent;
  }
  return list;
}

이 κΈ€μ—μ„œ 닀루지 μ•Šμ€ μš”μ²­ 쀑볡 λ°©μ§€λ‚˜ λ‹€λ₯Έ μš”κ΅¬μ‚¬ν•­λ„ μžˆμ§€λ§Œ, 제 κ²½ν—˜μƒ μœ„ κΈ°λŠ₯λ“€λ§Œ 좩쑱되면 λŒ€λΆ€λΆ„μ˜ λ ˆμ΄μ•„μ›ƒμ„ 컀버할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

@spilist
Copy link

spilist commented Apr 24, 2021

방법이 μ—†λ‚˜.. ν—ˆν—ˆ.. μ•„λ¬΄νŠΌ κ°μ‚¬ν•©λ‹ˆλ‹€ γ…Žγ…Ž

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment