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 사용 관련 설명 또한 추후에 첨부하겠음! 
 

플러터를 웹으로 배포시에 계속 같은 오류로 인해 자동 배포가 불가능했다 

해당 이슈는 플러터에서 사용되는 패키지들의 종속성이 업데이트되면서 발생하는 오류다. 

 

 

 

https://github.com/flutter/flutter/issues/143575 

 

[Web] App builds successfully, but then: "The type 'JSObject' can't be used as supertype." · Issue #143575 · flutter/flutter

Steps to reproduce Hi :) We have an app in production currently running 3.16.9 & now after upgrading to latest flutter I can't seem to run the app: flutter upgrade (3.16.9 => 3.19.0) flutter pub ge...

github.com

해결을 위해 플러터 깃 허브의 이슈를 찾아보니 

web 패키지를 올려라 내려라 올렷다가내려라 뭐 난리도 아니였다.... 

 

그중 해결에 도움을 중 방법은 

flutter pub upgrade web 을 통해서 web 버전을 올리는 것 

 

수동으로 배포할때는 이 방법을 쓰면 즉시 해결이되고, 

만약 나처럼 자동배포 yml 파일을 작성해야하는 경우에는 

 

해당 퍼블리시 파일 안에 

 

 

- run: flutter pub upgrade web

 

이 부분을 추가하면 된다 

 

 

어디에 넣어야하는지 헷갈린다면

 

ooo.yml 파일 안에 들어가서 

 

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - uses: subosito/flutter-action@v2
        with:
          channel: "stable"

      - run: flutter pub get

      - run: flutter pub upgrade web # **추가된 부분!! 

       .
       .
       .

      - run: flutter build web --base-href "/내 레포 주소/" # *본인의 repo 이름*으로 수정할 것!

 

요기쯤 넣어주면 되겠다. 

 

 

 

백오피스 시스템을 개발하면서 복잡한 기능 구현과 데이터 처리가 큰 도전과제였다.
특히 대용량 데이터를 효율적으로 관리하고, 비즈니스 로직을 유연하게 확장하며, 일관된 방식으로 유지해야 했다. 
 
이 과정에서 데이터 요청이 많고, 중복 요청을 유연하게 처리하기 위하여 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로 받아온다면 
더 높은 수준의 쿼리문을 작성할 수 있을 것이다 ~,~ 그 부분에 대해서는 추후 서술하겠음! 
 
 

1. inline과 inline block , block의 차이 

 

2. promise와 async/await의 차이 

에러처리 방식이 다르다

promise: .catch()

async/await : try-catch() 사용 

 

async/await이 Promise를 좀 더 쉽게 작성 할 수 있게 한 버전. 

promise는 비동기 처리를 위해 .then() .catch() .finally() 등 체인형태로 사용되어 

비동기 작업이 늘어나면 코드가 복잡해진다. 비동기작업 병렬실행 가능. 

 

둘 다 리턴값은 Promise객체이다. 

 

 

그러나 async/await은 비동기코드를 마치 동기코드처럼 작성할 수 있어 가독성이 높아진다. 

 

3. 자바스크립트 class 에서의 this의미 

>클래스의 생성된 인스턴스를 가리킴. 

 

4. let var const 의 차이

 

var가 가장 오래됨. ES5버전 이전에는 변수선언시 var만 사용했음.  변수가 선언된 함수 내에서만 유효하다.

전역변수로 쓰려면 함수 외부에서 선언해야. 

그러나 if와 for와 같은 블록스코프는 인식하지않음 블록(if or for) 내부에서 선언한 함수는 외부에서도 접근 가능하다. 

같은 스코프 내에서 재선언 가능하다. 

 

let, const 는 ES6부터 도입되었다.  var의 문제점을 해결하기위해 도입됨. 

둘 다 블록 스코프를 가지고, 선언된 블록 {} 안에서만 유효하다. 

같은 스코프 안에서 재선언 하는 것이 불가능하다. 

 

const는 상수, let은 변수

let은 재할당 가능, 그러나 const는 재할당 불가능. 그러나 const로 선언된 객체나 배열같은 참조형 데이터의 요소는 변경가능.  

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/176962#qna

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

function solution(plans) {
  var answer = [];
  var notFinish = [];

  let plan = plans.sort((a, b) => {
    const [aHours, aMinutes] = a[1].split(':').map(Number);
    const [bHours, bMinutes] = b[1].split(':').map(Number);
    return aHours * 60 + aMinutes - (bHours * 60 + bMinutes);
  });

  for (let idx = 0; idx < plan.length; idx++) {
    const p = plan[idx];
    const [hours, minutes] = p[1].split(':').map(Number);
    const date = new Date();
    date.setHours(hours, minutes, 0, 0);

    const playTime = parseInt(p[2]) * 60 * 1000;
    date.setTime(date.getTime() + playTime);

    if (idx + 1 < plan.length) {
      const [nextHours, nextMinutes] = plan[idx + 1][1].split(':').map(Number);
      const nextDate = new Date();
      nextDate.setHours(nextHours, nextMinutes, 0, 0); 

      if (nextDate < date) {
      
        notFinish.push([p[0], (date - nextDate) / (60 * 1000)]); 
      } else {
        answer.push(p[0]);

      
        let remainingTime = (nextDate - date) / (60 * 1000); 
        while (notFinish.length > 0 && remainingTime > 0) {
          const [task, timeLeft] = notFinish.pop(); 
          if (remainingTime >= timeLeft) {
            answer.push(task);
            remainingTime -= timeLeft;
          } else {
            notFinish.push([task, timeLeft - remainingTime]);
            break;
          }
        }
      }
    } else {
      answer.push(p[0]); 

      while (notFinish.length > 0) {
        answer.push(notFinish.pop()[0]);
      }
    }
  }

  return answer;
}

조건문지옥....................... 

 

스택을 활용해서 풀면 된다 

물론 자바스크립트는 스택이나 큐를 제공하지 않기 때문에 shift push pop 을 적절히 사용해서 배열을 스택처럼 써야한다. 

 

 

오답노트 : 

 

다음 과제 시작 전에 미뤄둔 과제를 처리해줘야하는데

미뤄둔 과제 리스트중에 첫번째 과제가 40분이고 다음 과제까지 남은시간이 20분이다

그러면 20분이라도 했다고 처리해줘야 문제가 해결되는거였음

 

처음에 풀때

남은시간은 20분인데 40분짜리 과제? 절대못하죠.  이러고 넘어가버려서 절반이 오류케이스로 나왔었다 

테스트케이스에 이런경우도 같이 고려해서 넣어주면 좋겠음. 설명이 부족한 문제인듯 

https://school.programmers.co.kr/learn/courses/30/lessons/160586

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

function solution(keymap, targets) {
    var answer = [];
    var map = new Map();
    keymap.forEach((keys) => {
    for (let idx = 0; idx < keys.length; idx++) {
      let k = keys[idx];
      if (!map.has(k)) {
        map.set(k, idx + 1);
      } else {
        if (map.get(k) > idx + 1) {
          map.set(k, idx + 1);
        }
      }
    }
  });
    
    targets.forEach((t) => {
    let ans = 0;

    for (let i = 0; i < t.length; i++) {
      if (!map.has(t[i])) {
        ans = -1;
        break;
      } else {
        ans += map.get(t[i]); 
      }
    }

    answer.push(ans); 
  });
    return answer;
}

 

keymap으로 넘어오는 값을 배열로 착각하고 forEach로 풀었더니 초반에 오류가 발생했는데

for 반복문으로 푸니까 바로 해결이 되었다 ㅎㅎ 간단한문제! 

코드 스플리팅이란? 
초기 로드 시점에 모든 코드를 다운로드 하지않고 필요한 컴포넌트만 로드하여 초기로딩 시간을 줄이고 성능을 개선하는 방법 (비동기 컴포넌트 랜더링!)
 
리액트에서는 
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(검색 엔진 최적화)에도 도움이 되고 
초기 로딩속도가 줄어드는 장점이 있음 ~,~ 
 
 

function solution(begin, target, words) {

    if (!words.includes(target)) return 0;

  
    let queue = [[begin, 0]]; 
    let wordAll = new Set(words); 
    let now = begin;
    let answer = 0;
 
    function findSimilar(word1, word2) {
        let diff = 0;
        for (let i = 0; i < word1.length; i++) {
            if (word1[i] !== word2[i])
                diff++;
            if (diff > 1) return false; 
        }
        return diff === 1; 
    }

  
    while (queue.length > 0) {
        let [current, step] = queue.shift();

      
        if (current === target) return step;

     
        for (let word of wordAll) {
            if (findSimilar(current, word)) {
                queue.push([word, step + 1]); 
                wordAll.delete(word); // 방문한 단어는 Set에서 제거
            }
        }
    }

  
    return 0;
}

아직도 이해가 잘 안 된다.........................

+ Recent posts