Next.js는 서버사이드랜더링과 정적 페이지 생성을 제공하는 React프레임워크이다 
리엑트를 기반으로 동작하기 때문에 리엑트에서 사용되는 기본적인 상태관리나 스타일 관련 라이브러리, 코드 번들러, babel 등을 동일하게 사용할 수 있어 리액트 개발자들이 쉽게 사용할 수 있다. 
 
Next에 대해 알기 전에는(어깨너머로 알 때는)
서버사이드랜더링을 써야하는 이유에 대해 이해하지 못했다.
어차피 유저 인터렉션 많은 페이지에서는 큰 의미가 없는게 아닌가 라고 생각했기 때문이다.

그러나 실제로 사용해보면서,  단순히 서버에서 페이지를 렌더링하는 것 이상의 다양한 성능 최적화와 기본 설정이 제공된다는 사실을 알게 되었다.

서버-클라이언트 구조가 사용자 경험에 긍정적인 영향을  미치는지를 경험하면서, 왜 많은 회사에서 Next를 사용하게 되었는지 이해했다.


내가 알게된 내용에 대해 요약하고, 코드에 적용한 것을 아래에 정리해봤다.



 
1.서버사이드 랜더링과 정적 페이지 생성
데이터가 실시간으로 업데이트 되지 않는 경우, 매번 계속 페이지를 생성하는 것은 비효율적이다. 이럴때
 

  • ISR 을 사용하면 데이터가 갱신될 때만 페이지가 새로 생성하거나, 
  • SSG를 통해 한 번 생성된 페이지를 정적으로 제공해서 서비스의 성능을 높일 수 있다. 
  • 또한 SSR이 기본적으로 제공되기 때문에 서버에서 미리 html을 랜더링해와 초기 로딩속도를 줄일 수 있다. -> SEO최적화에 도움됨. 


** ISR과 SSG의 차이**

- ISR (incremental static regeneration):특정 주기(revalidate)로 페이지를 갱신함. 특정 주기마다 데이터가 업데이트 되는 경우 적합하다. e.g) 뉴스 등 

- SSG (static site generation) : 빌드 타임에 모든 페이지를 정적으로 미리 생성한다. 페이지가 빌드될 때 한 번 생성하고, 그 이후에는 동일한 정적 파일을 계속 제공한다. 데이터를 업데이트하기위해서는 사이트 전체를 다시 빌드해야한다.
자주 변하지 않는 서비스에 적합.  e.g)블로그나 기술 문서 등 
 
 
2. 파일 기반 라우팅
Next.js의 파일 기반 라우팅(App Routing)은 파일 경로만 설정하면 자동으로 URL이 생성되기 때문에 기존 리액트에 비해 프로젝트의 구조가 훨씬 간결해진다. 폴더명이 url이 되는 형식.
 
e.g) App/News/Page.js 의 구조일 때 myappUrl/News 로 접속하면 news 폴더 안의 pages.js 내용이 랜더링 된다. 
 
 
3. 성능 최적화
Next.js는 기본적으로 코드 스플리팅이미지 최적화를 제공하기 때문에 로딩 속도 최적화가 가능하다. 단순히 기능을 제공할 뿐만 아니라, 유저 사용성을 높이기 위해서는 next를 쓰는 것이 좋다. 물론 기본 리엑트에서도 스플리팅과 이미지 최적화 모두 가능하지만 설정이 필요하다. 
 
 
 


스팀에서 제공하는 api를 사용해서 간단한 서버사이드 랜더링 페이지를 구현 해보자 !
 
 
파일 구조(root)
 

App 폴더

ㄴ page.tsx

Component 폴더

ㄴ gamelist.tsx

ㄴ newslist.tsx

API 폴더

ㄴ api.tsx

 
 
 
page.tsx  : 서버 사이드 렌더링을 통해 Steam API 데이터를 가져오는 메인 페이지  
현재는 내 게임정보와 스팀 뉴스를 가져오고 있다.

  • 서버 컴포넌트에서 가져온 데이터를 클라이언트 컴포넌트로 넘겨줌 으로서 빠른 초기 로딩, SEO 최적화라는 장점이 있다. 
import { fetchMyGames, fetchSteamNews } from "@/api/getSteamApi";
import GameList from "@/components/GameList";
import SteamNewsList from "@/components/NewsItem";

export default async function HomePage() {
  const news = await fetchSteamNews();
  if (!news || news.length === 0) {
    return <p>No news available</p>;
  }

  const myGame = await fetchMyGames();

  return (
    <div>
      <h3>Steam News</h3>
      <SteamNewsList news={news} /> 
      <h3>You Played</h3>
      <GameList games={myGame} />
    </div>
  );
}

 
 
 
api.tsx : 스팀에서 내 개임 정보를 가져오는 api .
api와 id는 .env.local에서 환경변수 정의 후 가져오면 된다. 

export async function fetchMyGames() {
  const apiKey = process.env.API_KEY;
  const steamId = process.env.MY_STEAM_ID;
  const url = `url 생략...`;
  try {
    const res = await fetch(url);

    if (!res.ok) {
      throw new Error(
        `Mygame API url을 가져올 수 없습니다: code ${res.status}`
      );
    }

    const data = await res.json();

    const games: Game[] = data.response.games;

    if (games && games.length > 0) {
      console.log(`total ${games.length} games:`);
    } else {
      console.log("total 0 games");
    }

    return games; // 게임 목록 반환
  } catch (error) {
    console.error("[error] Fetch game info failed:", error);
    return [];
  }
}

 
gamelist.tsx : 스팀 아이디를 통해 플레이한 게임의 정보를 가져오는 코드 
 

  • 서버 컴포넌트에서 데이터를 가져오고, 클라이언트 컴포넌트에서 UI를 렌더링하는 방식으로 서버와 클라이언트가 분리됨. 서버 부하를 줄이고 성능을 최적화 할 수 있다. 
import React from "react";
import { Game } from "./type/type";
function GameIcon({
  appid,
  img_icon_url,
}: {
  appid: number;
  img_icon_url: string;
}) {
  const imageUrl = `https://media.steampowered.com/steamcommunity/public/images/apps/${appid}/${img_icon_url}.jpg`;

  return (
    <img
      src={imageUrl}
      alt="Game Icon"
      width="50"
      height="50"
      style={{ borderRadius: 50 }}
    />
  );
}

export default function GameList({ games }: { games: Game[] }) {
  return (
    <ul>
      {games.map((game) => (
        <li
          key={game.appid}
          style={{ listStyle: "none", marginBottom: "10px" }}
        >
          <h4>{game.name}</h4>
          <GameIcon appid={game.appid} img_icon_url={game.img_icon_url} />
          <p>Playtime: {Math.floor(game.playtime_forever / 60)} hours</p>
        </li>
      ))}
    </ul>
  );
}

 
 
 
 
 
지금까지 서버 -클라이언트 컴포넌트를 분리하여 최적화된 페이지를 만드는 법을 정리해보았다.
 
현재 코드는 페이지가 하나로 파일 기반 라우팅이 적용되지 않았다. 
코드가 업데이트되는대로 해당 부분을 업로드하겠다.
 
더불어 뉴스 컴포넌트나 스팀 api 사용 관련 설명 또한 추후에 첨부하겠음! 
 

백오피스 시스템을 개발하면서 복잡한 기능 구현과 데이터 처리가 큰 도전과제였다.
특히 대용량 데이터를 효율적으로 관리하고, 비즈니스 로직을 유연하게 확장하며, 일관된 방식으로 유지해야 했다. 
 
이 과정에서 데이터 요청이 많고, 중복 요청을 유연하게 처리하기 위하여 React Query(Tanstack Query)를 사용하게 되었다. 
리액트 쿼리를 통해 데이터를 빠르게 가져오고, 캐시를 통해 중복 요청을 줄이면서 서비스를 최적화할 수 있었다. 
 
이번 글에서는 백오피스 시스템에서 복잡한 데이터 처리 기능을 효율적으로 관리하고, 성능을 최적화 했는지에 대해 공유하겠다! 
 
 

 


 
 
 
 
코드의 일관성과 유지보수성을 높이기 위해, 쿼리 키와 API 호출 로직을 별도로 분리했다. 이를 통해 팀원들과 코드 스타일을 맞추고, 유지보수가 쉬운 코드를 작성할 수 있었다.
또한 Axios를 적용하여 HTTP 요청을 간결하게 처리하고, 응답 데이터를 자동으로 JSON으로 파싱하여 쉽게 다루도록 했다. (Axios를 사용하면 JSON으로 응답이 돌아와서 추가 처리가 필요 없다!)
 
 
 
 
** 예시를 들기 위해 작성한 코드 입니다   ~.~  실제 업무용 코드 x 
 
쿼리 키 작성 

const employeeKey = {
  employee: ['employee'] as const,
  employeeWithID: (id: string) => ['employee', { id }] as const,
};

 
쿼리 키를 따로 파일로 분리하면 팀원들이 중복된 키를 작성하여 잘못된 데이터를 가져오거나 전송하는 문제를 방지할 수 있다. 유지보수 시에도 편리하므로 쿼리 키는 별도로 관리하는 것이 좋다.
 
 
 
커스텀 훅 작성 
 

import { useQuery } from 'react-query';
import axios from 'axios';
import { employeeKey } from "./employeeQueryKey";

// API 호출 함수 (전체 또는 개별 사원 정보 패칭)
const fetchEmployees = (id?: string) => 
  axios.get(`/api/employees${id ? `/${id}` : ''}`).then(res => res.data); //api 주소를 여기에 넣으면 됨

// useEmployeeInfo 커스텀 훅
const useEmployeeInfo = (employeeId?: string) => {
  const { data, isError, isLoading, error } = useQuery({
    queryKey: employeeId ? employeeKey.employeeWithID(employeeId) : employeeKey.employee,
    queryFn: () => fetchEmployees(employeeId), // employeeId가 없으면 undefined 전달
    staleTime: 1000 * 60 * 10, //캐시 지속 시간 
  });

  return {
    fetchData: data, // 패칭된 데이터
    isFetchDataLoading: isLoading, // 로딩 상태
    isFetchDataError: isError, // 에러 상태
    fetchError: error, // 에러 정보
  };
};

 
 
 
 
이제 컴포넌트에서 위에서 구현한 커스텀 훅을 통해 데이터를 가져오자 

const EmployeeInfo = ({ employeeId }: { employeeId: string }) => {
  // 사원 ID를 넘겨서 특정 사원의 정보를 패칭
  const { fetchData, isFetchDataLoading, isFetchDataError, fetchError } = useEmployeeInfo(employeeId);

  if (isFetchDataLoading) return <div>Loading employee data...</div>;
  if (isFetchDataError) return <div>Error loading data: {fetchError?.message}</div>;

  return (
    <div>
      <h1>Employee Name: {fetchData.name}</h1>
      <p>Position: {fetchData.position}</p>
      <p>Department: {fetchData.department}</p>
    </div>
  );
};

 
 
 
여기서 useQuery안에 들어가는 옵션 등을 props로 받아온다면 
더 높은 수준의 쿼리문을 작성할 수 있을 것이다 ~,~ 그 부분에 대해서는 추후 서술하겠음! 
 
 

코드 스플리팅이란? 
초기 로드 시점에 모든 코드를 다운로드 하지않고 필요한 컴포넌트만 로드하여 초기로딩 시간을 줄이고 성능을 개선하는 방법 (비동기 컴포넌트 랜더링!)
 
리액트에서는 
React.lazy와 Suspense를 사용해 구현이 가능하다. 
둘 다 16.6버전에서 처음 도입되었고
18버전부터는 Suspense의 기능이 더욱 강회되어서 비동기 데이터 처리에도 사용이 가능해졌다! 
 
 
Suspense와 Lazy 사용법

import React, { Suspense, lazy } from 'react';

.... {

const LazyComponent = lazy(() => import('./로드가 오래 걸리는 컴포넌트명'));
.
.
.
return <Suspense fallback={<div>Loading component...</div>}> <LazyComponent/></Suspense>

}

 
이렇게 구현하면 초기 번들에 모든 코드가 포함되는것이 아니라 
해당 컴포넌트가 필요한 시점에 로드된다! 
 
SEO(검색 엔진 최적화)에도 도움이 되고 
초기 로딩속도가 줄어드는 장점이 있음 ~,~ 
 
 

 
[ 테스트 코드란? ]
 
컴포넌트 안에 들어가는 기능(onClick 이벤트 등 핸들러)과 전체적인 동작이 개발자의 의도대로 동작하는지 확인하는 코드 . jest는 Meta에서 개발한 js 테스트 프레임워크로 리액트와의 호환성이 뛰어나 리액트 프로젝트에서 많이 사용된다! 
리액트 외에 Vue.js, Angular에서도 사용 가능하다 
 
 
 
테스트 코드를 작성하는데 도움을 받은 문서 
 
리액트에서의 테스트 방안 한글문서 
 
https://ko.legacy.reactjs.org/docs/testing-recipes.html#mocking-modules

테스팅 방안 – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

 
 
getting started - jest 
 
 
https://jestjs.io/docs/getting-started

Getting Started · Jest

Install Jest using your favorite package manager:

jestjs.io

 
 
 
jest 설치 방법(with Typescript)
 
npm이나 yarn을 통해 jest와 관련 패키지를 설치한다.

npm install --save-dev jest @testing-library/jest-dom @testing-library/react @types/jest
npm install @testing-library/user-event

 
패키지에 대한 설명 
 
jest : 테스트 프레임워크 
@testing-library/jest-dom : dom 관련 테스트 메쳐 추가제공  *테스트 메쳐: 테스팅프레임워크에서 사용되는 함수. 
@testing-library/react : 리액트 컴포는트를 테스트하기 위한 유틸리티 제공 
@types/jest : 타입스크립트에서 jest를 사용할때 필요한 타입 제공 
@testing-library/user-event  사용자이벤트를 시뮬레이션하여 테스트할수있게 함 . 
 
 
테스트 코드를 작성 후 
콘솔에서 npm test 를 입력하면 테스트코드가 돌아간다. 
 
 
jest의 매쳐(matcher)를 사용해서 테스트를 진행해보자 
 
 
describe : 테스트케이스들을 그룹화 하는 함수 
 
it or test : 개별 테스트 케이스를 작성하는 함수. 둘 다 동일한 기능. describe 안에서 여러개를 작성 할 수도 있고, describe 없이 하나만 작성하는 것도 가능. 그러나 afterEach, beforeEach 등의 기능을 쓰기위해서는 describe안에 정의해야만 한다. 
 
afterEach , beforeEach: 테스트 실행 전,후에 실행될 동작을 정의하는 함수. 테스트케이스별로 각각 동작한다. 
 
afterAll , beforeAll: describe 그룹 내의 모든 테스트 가 동작하기 전,후로 한 번씩만 실행될 동작을 정의하는 함수. 
 
expect : 테스트의 결과를 검증할때 사용. 특정 함수가 예상대로 동작하는지 확인하는 함수.
e.g) expect(어떤value).toBe(어쩌고handler); => 어떤 value가 true일때 어쩌고 핸들러가 동작해야 한다. 
 
 
테스트코드의 파일명은 
(파일명).test.ts 등으로 작성하는 것이 일반적이다. 
 
왜나하면, jest가 테스트 파일로 자동 인식하는 파일명 패턴이 정해져있기 때문이다! 
 
패턴 종류
.test.js .test.jsx .test.ts .test.tsx
.spec.js .spec.jsx .spec.ts .spec.tsx
 
 
이제 위에 소개한 메쳐들이 들어간 테스크 코드를 jest 공식문서에 나오는 코드를 사용해서 작성해보자 
 
 
파일명 예시 : mathHook.test.ts 

describe("덧셈 뺄셈 테스트 코드", ()=>{


beforeEach(()=>{
//각각의 테스트 코드 실행 전에 실행할 내용을 작성 
//e.g) 변수 초기화 등 
});

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});


it('four minus two is two', () => {
  expect(4 - 2).toBe(2);
});


afterEach(()=>{
//각각의 테스트 코드 실행 후에 실행할 내용을 작성 
//e.g) 리소스 해제 등 메모리 할당 해제 
});

})

 
 
- describe를 통해 덧셈 테스트코드, 뺄셈 테스트코드를 하나의 그룹으로 묶고 
- it or test를 사용해 각각의 테스트코드를 작성한다. 
- expect를 통해 기대되는 값을 확인한다. 
 
 
 
 
앞으로 더 추가할 예정.. 
 
 
 
 

요일

const today = new Date();
const day = today.getDay();
// 일(0)~토(6)순으로 화요일의경우 2

 

const today = new Date();
const date = today.getDate();

 

const today = new Date();
const month = today.getMonth();
// 월의 경우 실제 월보다 -1된 값을 리턴한다 
//ex) 1월의 경우 0, 10월의경우 9를 리턴함. 31일 여부를 체크할 때 등 주의!!

 

연도

const today = new Date();
const day = today.getFullYear();
// getYear는 웹 표준에서 사라졌으므로 getFullYear사용

 

 

+ Recent posts