본문 바로가기
프론트엔드

요즘 프론트엔드 개발자가 알아야 할 것들: 모노레포, FSD, 서버 상태 맛보기

by 수목원 2026. 10. 13.

요즘 프론트엔드 개발자가 다루는 범위는,

React로 화면을 만드는 일에서 끝나지 않습니다.

 

프로젝트 구조와 빌드, 서버 상태, 디자인 시스템까지 함께 등장합니다.

 

기술 이름을 외우는 것만으로는 실제 프로젝트에 적용하기 어렵습니다.

 

그래서 각 기술이 어떤 문제를 해결하는 개념인지,

왜 필요한지, 어디부터 적용할 수 있는지 맛보기로 정리합니다.

 

이 글에서 다루는 기술은 다음과 같습니다.

 

  • pnpm 모노레포와 Turborepo: 여러 앱의 코드 공유, 변경된 패키지만 다시 빌드
  • React 19 + TypeScript + Tailwind CSS v4: Next.js와 Vite SPA 중 프로젝트 성격에 맞게 선택
  • FSD(Feature-Sliced Design): 레이어 단위 기능 분리, 공개 API로 의존 방향 고정
  • TanStack Query와 Query Key Factory: 쿼리 키 체계화, 필요한 범위만 invalidate
  • React 컴파일러(React Compiler): 수동 메모이제이션 부담 감소
  • 디자인 시스템과 Storybook: 공용 컴포넌트 문서화, 상태별 화면 확인

여러 앱이 코드를 공유하는 경계,

한 앱 안에서 기능을 나누는 경계,

서버 데이터를 다시 가져오는 경계를 차례로 살펴봅니다.

 

1) pnpm 모노레포와 Turborepo

모노레포(Monorepo)는 여러 앱과 패키지를 하나의 저장소에서 관리하는 방식입니다.

 

쇼핑몰과 관리자 앱이 같은 버튼, 상품 타입, 린트 설정을 사용한다면,

이를 공용 패키지로 분리할 수 있습니다.

 

앱마다 복사한 버튼을 따로 수정하는 대신,

공용 패키지를 고치고 두 앱의 영향을 한 변경에서 확인합니다.

 

같은 저장소에 있다는 이유만으로 모든 코드가 공유되는 것은 아닙니다.

 

각 패키지는 자신의 package.json을 가지고,

다른 패키지를 사용할 때 의존성을 명시해야 합니다.

 

pnpm의 워크스페이스(Workspace)는 이 패키지들을 함께 관리하는 단위입니다.

 

루트의 pnpm-workspace.yaml에서 포함할 경로를 지정합니다.

아래는 실행 결과가 아닌 설정 예시입니다.

 

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

apps/store, apps/admin에는 실행할 앱을 두고,

packages/ui, packages/config에는 공용 코드를 둡니다.

 

관리자 앱이 @repo/ui를 사용한다면,

의존성 버전을 workspace:*로 선언할 수 있습니다.

 

이 선언은 레지스트리의 동명 패키지 대신,

워크스페이스 안의 패키지를 사용하도록 요구합니다.

공식 워크스페이스 문서

 

pnpm이 패키지 설치와 연결을 맡는다면,

Turborepo는 여러 패키지의 작업 순서와 캐시를 관리합니다.

 

앱이 UI 패키지에 의존할 때,

UI 빌드를 먼저 끝내고 앱을 빌드하도록 설정할 수 있습니다.

 

{
  "$schema": "https://turborepo.dev/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    }
  }
}

^build는 의존하는 패키지의 build 작업을 먼저 실행한다는 뜻입니다.

각 패키지에는 해당 이름의 스크립트가 있어야 합니다.

공식 설정 문서

 

빌드 캐시는 의존 그래프(Dependency Graph)와 작업 입력을 바탕으로 판단합니다.

 

소스와 설정, 의존 작업의 결과 등이 같다면,

이전 작업의 출력물을 복원해 빌드를 생략합니다.

 

UI 변경은 이를 사용하는 앱에 영향을 주지만,

관련 없는 패키지의 작업까지 항상 다시 실행할 필요는 없습니다.

 

다만 빌드에 영향을 주는 환경 변수나 파일이 캐시 입력에서 빠지면,

잘못된 결과를 재사용할 수 있습니다.

 

outputs는 복원할 산출물 경로이며,

외부 입력까지 자동으로 알아내는 설정은 아닙니다.

공식 캐시 문서

 

공유할 코드와 함께 검증할 앱이 실제로 있을 때,

모노레포의 관리 비용을 감수할 이유가 생깁니다.

 

2) React 19 + TypeScript + Tailwind CSS v4, Next.js와 Vite SPA

React 19는 화면과 상호작용을 구성하고,

TypeScript는 컴포넌트와 데이터 사이의 타입 계약을 표현합니다.

 

Tailwind CSS v4는 유틸리티 클래스와 테마로 스타일을 구성합니다.

 

이 조합을 정해도,

화면을 어디서 렌더링하고 어떻게 배포할지는 따로 선택해야 합니다.

 

Next.js의 앱 라우터(App Router)는 라우팅과 레이아웃,

서버 컴포넌트(Server Component)를 함께 제공합니다.

 

검색에 노출할 상품 상세나 게시글처럼,

첫 응답에 콘텐츠와 메타데이터가 필요한 화면에서 검토할 만합니다.

 

서버에서 데이터를 읽어 화면을 만들고,

입력과 클릭이 필요한 부분을 클라이언트 컴포넌트(Client Component)로 나눕니다.

공식 서버/클라이언트 컴포넌트 문서

 

'use client'는 클라이언트 코드의 경계를 선언합니다.

처음부터 브라우저에서만 렌더링한다는 뜻은 아닙니다.

 

최상위 화면 전체에 선언하기보다,

상태와 이벤트가 필요한 부분부터 경계를 잡는 편이 이해하기 쉽습니다.

 

로그인 뒤 사용하는 대시보드라면,

Vite 기반 단일 페이지 앱(SPA)도 적절한 선택입니다.

 

브라우저에서 API를 호출하고 화면을 전환하는 구조로 시작할 수 있습니다.

 

검색 노출보다 필터, 표, 편집 같은 상호작용이 중요하고,

별도 백엔드가 있다면 구성을 단순하게 유지하기 좋습니다.

 

Vite는 개발 서버와 빌드 도구입니다.

라우터와 인증 흐름, 데이터 접근 방식은 별도로 구성합니다.

공식 Vite 안내

 

Next.js로 대시보드를 만들 수도 있고,

Vite를 활용하는 서버 렌더링 구성도 가능합니다.

 

선택 기준은 프레임워크 이름보다,

공개 콘텐츠의 비중과 서버의 역할, 배포 환경에 두는 편이 낫습니다.

 

TypeScript 타입도 서버 응답의 실제 값을 검증하지는 않습니다.

외부 응답 검증과 오류 처리는 별도로 설계해야 합니다.

 

3) FSD: 레이어와 읽기/쓰기 분리

기능 분할 설계(Feature-Sliced Design, FSD)는,

코드를 업무 의미와 의존 방향에 따라 나누는 구조입니다.

 

components, hooks, utils만으로 나누면,

상품 수정에 필요한 코드가 여러 폴더에 흩어지기 쉽습니다.

 

FSD는 레이어(Layer)로 책임의 높이를 정하고,

슬라이스(Slice)로 상품이나 장바구니 같은 업무 단위를 나눕니다.

 

슬라이스 안에서는 ui, model, api 같은 세그먼트(Segment)로 역할을 구분합니다.

 

  • app: 앱 초기화, 전역 프로바이더, 라우팅 연결
  • pages: 페이지 단위 화면과 기능 조합
  • widgets: 여러 요소를 묶은 독립적인 화면 블록
  • features: 장바구니 담기, 상품 수정 같은 사용자 행동
  • entities: 상품, 사용자 같은 업무 데이터와 표현
  • shared: 업무에 종속되지 않는 UI, 유틸리티, 통신 기반 코드

FSD 레이어와 위에서 아래로 향하는 의존 방향.

 

기본적으로 상위 레이어는 하위 레이어를 사용할 수 있고,

하위 레이어가 상위 레이어를 가져오지는 않습니다.

 

같은 레이어의 서로 다른 슬라이스도 직접 의존하지 않는 것이 기본 규칙입니다.

공식 FSD 개요

 

이번 구조에서는 읽기는 entities, 쓰기는 features에 둡니다.

 

상품 타입과 조회 함수, 조회 키는 entities/product에 두고,

상품 수정 폼과 변경 요청은 features/edit-product에 둡니다.

 

수정 기능은 상품의 공개 조회 키를 사용해,

저장 성공 뒤 필요한 캐시를 무효화합니다.

 

상품을 보여주기만 하는 화면이,

수정 폼과 변경 로직까지 의존하지 않도록 만드는 구분입니다.

 

이 읽기/쓰기 분리(Read/Write Separation)는 적용 규칙입니다.

모든 조회가 반드시 entities에 속한다는 FSD의 절대 규칙은 아닙니다.

 

한 페이지에서만 쓰는 조회와 로직은,

해당 페이지에 두고 공유 필요가 생겼을 때 추출할 수 있습니다.

 

외부에 공개할 항목은 슬라이스의 index.ts에서만 내보냅니다.

 

// entities/product/index.ts
export { productKeys } from './api/product-keys';
export { ProductCard } from './ui/product-card';
export type { Product } from './model/types';

외부 코드는 entities/product를 가져오고,

내부 파일 경로를 직접 가져오지 않도록 합니다.

 

이 공개 API(Public API)가 있어야,

내부 파일을 옮겨도 사용하는 쪽의 변경을 줄일 수 있습니다.

공식 공개 API 설명

 

shared는 슬라이스가 없으므로,

shared/ui 같은 세그먼트별 index.ts를 공개 경계로 삼을 수 있습니다.

 

4) TanStack Query, Query Key Factory와 범위를 좁힌 invalidate

서버 상태(Server State)는 브라우저 밖에 원본이 있는 데이터입니다.

 

상품 가격은 다른 사용자가 바꿀 수 있고,

화면을 켜 둔 사이에도 오래된 값이 될 수 있습니다.

 

로딩과 오류 외에도 캐시 공유,

재요청 시점, 변경 뒤 갱신 범위를 관리해야 합니다.

 

TanStack Query는 이 서버 데이터의 조회와 캐시를 관리합니다.

 

모달 열림이나 저장 전 입력값 같은 클라이언트 상태(Client State)는,

컴포넌트 상태 등으로 따로 관리합니다.

 

쿼리 키(Query Key)는 캐시의 주소입니다.

조회 결과가 달라지는 필터, 페이지 번호, 상품 ID를 키에 포함해야 합니다.

 

키 팩토리(Query Key Factory)는 이를 한곳에서 계층적으로 만드는 방식입니다.

별도 라이브러리가 없어도 함수와 객체로 구성할 수 있습니다.

 

아래 코드는 @tanstack/query-core 5.104.2와 Node 20에서 실행한 예시입니다.

서버 요청 없이 캐시를 채우고, 무효화 범위만 확인합니다.

 

import { QueryClient } from '@tanstack/query-core';

// Query Key Factory: 키를 한 곳에서 계층적으로 만든다
export const productKeys = {
  all: ['products'],
  lists: () => [...productKeys.all, 'list'],
  list: (filter) => [...productKeys.lists(), filter],
  details: () => [...productKeys.all, 'detail'],
  detail: (id) => [...productKeys.details(), id],
};

const qc = new QueryClient();
qc.setQueryData(productKeys.list({ cat: 'shoes' }), ['A', 'B']);
qc.setQueryData(productKeys.list({ cat: 'bags' }), ['C']);
qc.setQueryData(productKeys.detail(1), { id: 1, price: 100 });
qc.setQueryData(productKeys.detail(2), { id: 2, price: 200 });

// 상품 1의 가격을 바꾼 뒤: 상품 1 상세 + 목록만 무효화
await qc.invalidateQueries({ queryKey: productKeys.detail(1) });
await qc.invalidateQueries({ queryKey: productKeys.lists() });

for (const q of qc.getQueryCache().getAll())
  console.log(JSON.stringify(q.queryKey).padEnd(36), q.state.isInvalidated ? 'stale(무효화)' : 'fresh');

직접 돌려보니 출력은 다음과 같습니다.

 

["products","list",{"cat":"shoes"}]  stale(무효화)
["products","list",{"cat":"bags"}]   stale(무효화)
["products","detail",1]              stale(무효화)
["products","detail",2]              fresh

상품 1 상세와 목록 키만 선택한 무효화 범위.

 

lists()는 ['products', 'list']를 반환합니다.

기본 접두사 매칭으로 신발과 가방 목록이 모두 선택됩니다.

 

detail(1)은 상품 1 상세를 선택하므로,

상품 2 상세인 detail(2)는 fresh로 남습니다.

 

이 변경 때문에 상품 2까지 무효화하지 않아,

불필요한 재요청을 유발하지 않습니다.

 

다만 출력의 fresh는 isInvalidated가 거짓이라는 뜻입니다.

시간이 지나도 데이터가 항상 최신이라는 보장은 아닙니다.

 

실제 화면에서는 staleTime과 마운트,

포커스 복귀 같은 조건이 재요청에 영향을 줍니다.

 

무효화(Invalidation)는 캐시 삭제도, 모든 쿼리의 즉시 재요청도 아닙니다.

 

기본 동작에서는 화면에서 사용하는 활성 쿼리를 백그라운드로 재요청하고,

비활성 쿼리는 무효화된 상태로 남습니다.

공식 무효화 문서

 

이 예시에는 활성 관찰자와 조회 함수가 없으므로,

출력은 네트워크 요청 횟수를 증명하지 않습니다.

 

가격 변경은 목록의 표시 값이나 정렬에도 영향을 줄 수 있어,

예시에서는 모든 상품 목록을 갱신 대상으로 잡았습니다.

 

실제 적용에서는 변경 성공 후에 무효화하고,

어떤 화면의 결과가 달라지는지 기준으로 범위를 결정합니다.

 

5) React 19 컴파일러와 수동 메모이제이션

메모이제이션(Memoization)은 입력이 같을 때,

이전에 계산한 값이나 렌더링 결과를 재사용하는 최적화입니다.

 

기존에는 useMemo, useCallback, React.memo로,

계산 결과와 함수 참조, 컴포넌트 렌더링을 직접 관리했습니다.

 

React Compiler는 빌드 과정에서 코드를 분석해,

이 수동 메모이제이션에 해당하는 최적화를 자동으로 적용합니다.

공식 React Compiler 소개

 

React 19를 설치하는 것만으로 컴파일러가 자동 활성화되지는 않습니다.

사용하는 빌드 도구나 프레임워크에서 컴파일러를 설정해야 합니다.

 

또한 상태와 props를 직접 변경하지 않는 등,

React의 규칙을 지켜야 분석과 최적화가 안전하게 작동합니다.

 

컴파일러를 사용하는 코드에서는 수동 메모이제이션을 먼저 추가하기보다,

측정으로 문제가 확인된 경우에만 검토합니다.

 

프로파일러(Profiler)에서 느린 상호작용과 비용이 큰 부분을 찾고,

변경 전후에 같은 동작을 비교합니다.

 

오래 걸리는 계산 자체를 줄여야 할 수도 있고,

긴 목록에 가상화(Virtualization)가 필요할 수도 있습니다.

 

컴파일러의 캐시는 컴포넌트 계산을 위한 것이며,

앞서 다룬 서버 데이터 캐시를 대신하지 않습니다.

 

6) 디자인 시스템과 Storybook

디자인 시스템(Design System)은 색과 버튼 모음에 더해,

사용 규칙과 상태, 접근성 기준을 함께 관리하는 체계입니다.

 

두 앱이 같은 버튼을 사용하더라도,

로딩 표시와 비활성 조건이 다르면 사용자 경험은 달라집니다.

 

공용 UI 패키지에는 기본 컴포넌트와 디자인 토큰(Design Token)을 두고,

상품 수정 같은 업무 행동은 앱의 기능 코드에 둡니다.

 

그래야 공용 버튼이 특정 앱의 API나 권한 구조에 묶이지 않습니다.

 

Storybook의 스토리(Story)는,

컴포넌트의 특정 상태를 독립적으로 보여주는 예시입니다.

 

기본 버튼, 로딩 버튼, 오류가 있는 입력을 스토리로 만들면,

앱의 특정 화면까지 이동하지 않고 확인할 수 있습니다.

 

스토리는 문서와 테스트의 공통 재료가 됩니다.

 

사용 예시를 문서로 제공하고,

play 함수와 테스트 설정을 더해 클릭이나 오류 표시를 검증할 수 있습니다.

공식 Storybook 테스트 문서

 

스토리를 작성했다는 사실만으로 모든 동작이 검증되지는 않습니다.

확인할 행동과 기대 결과를 함께 정의해야 합니다.

 

Figma의 색상과 간격 토큰은,

이름과 단위를 정리해 Tailwind CSS v4의 @theme 변수로 연결할 수 있습니다.

 

@import "tailwindcss";

@theme {
  --color-brand: #2563eb;
  --spacing-control: 0.75rem;
  --radius-control: 0.5rem;
}

이 변수는 bg-brand, p-control, rounded-control 같은 유틸리티로 이어집니다.

공식 테마 변수 문서

 

Figma 값을 CSS에 자동으로 연결해 주는 기능은 아닙니다.

토큰 내보내기와 변환 과정을 직접 구성하거나 도구로 연결해야 합니다.

 

토큰 변경 시 공용 패키지와 스토리를 함께 확인하면,

디자인 변경이 어떤 컴포넌트 상태에 영향을 주는지 검토하기 쉽습니다.

 

맺으며

여기서 다룬 개념들은 코드가 늘어날 때,

변경이 영향을 주는 범위를 설명하는 데 도움이 됩니다.

 

앱과 공용 패키지 사이에서는 의존성을,

기능 사이에서는 공개 API를,

서버 데이터에서는 쿼리 키와 갱신 범위를 정합니다.

 

모든 것을 한 번에 도입할 필요는 없습니다.

 

지금 프로젝트에서 가장 불편한 경계 하나를 골라,

작게 적용해 보는 것부터 시작하면 충분합니다.