Skip to content

seongmin36/ReFit-Lab-FE

 
 

Repository files navigation

다시입다연구소 로고

ReFit Lab Frontend

ICT 콤플렉스(ICT COC) '피우다 프로젝트 15회' 공모전 결선 진출 | 팀명 '다시입을Lab'


♻️ ReFit Lab : AI로 의류 교환을, 데이터로 지구를!

AI 기반 의류 교환의 환경 기여 및 사회적 가치 전환, 지속가능 의생활 플랫폼 ReFit Lab의 프론트엔드 저장소입니다.


📌 ReFit Lab은 이런 서비스예요

ReFit Lab은 다시입다연구소21% Party(의류 교환 행사)를 디지털화한 지속가능 의생활 플랫폼입니다.

21% Party는 '입지 않는 옷을 가져와 다른 사람의 옷과 교환'하는 행사로, 2024년 기준 51회 개최 · 누적 참가자 6,435명 · 교환율 73.67%를 기록한 검증된 순환 경제 모델입니다. ReFit Lab은 이 오프라인 경험을 AI와 결합해 온라인으로 확장합니다.

  • 파티 예약: 21% Party에 참가 신청하고 QR 입장권으로 간편하게 입장
  • 의류 교환: AI 기반 매칭으로 내 옷과 잘 어울리는 교환 상대를 추천
  • 행사 관리: 다가오는 행사(Upcoming)와 종료된 행사(Ended)를 한눈에 확인하고 예약
  • 마이페이지: 참가한 행사 히스토리, QR 티켓, 작성한 글을 한 곳에서 관리
  • 관리자 대시보드: 행사 등록·관리, 예약 현황·참가 통계를 차트로 시각화 (웹뷰)
  • 커뮤니티 & 챌린지: 지속가능 의생활 실천자들과 소통하고 챌린지에 참여

"오래된 미래를 입는다" — 다시입다연구소의 철학을 디지털로 구현한 플랫폼


🚀 로컬 실행

npm install
npm run dev
# http://localhost:3000

서버 없이 확인 가능한 기능:

  • 페이지 레이아웃 및 공통 UI 컴포넌트
  • 관리자 대시보드 차트 UI (Recharts)

백엔드 필요:

  • 파티 예약 및 행사 API 연동
  • 마이페이지 (QR 티켓, 파티 히스토리, 작성한 글 목록)
  • 인증 (로그인 / 로그아웃)
  • 관리자 행사 등록 및 예약 현황 조회

💻 기술 스택

역할 종류 선정 이유
Framework App Router 기반 파일 시스템 라우팅과 서버 컴포넌트로 초기 로드 최적화, Next.js API Route로 내부 API 분리 가능
Programming Language 정적 타입으로 API 응답·컴포넌트 props 오류를 컴파일 시점에 차단, 팀 협업 시 인터페이스 계약을 명확히 유지
Styling 유틸리티 클래스 기반으로 CSS 파일 분리 없이 빠른 UI 반복, clsx + tailwind-merge로 조건부 스타일링을 타입 안전하게 처리
Data Fetching 인터셉터로 publicAPI · privateAPI · internalAPI 세 인스턴스를 분리, 요청/응답 단계에서 토큰 주입과 401 처리를 일관되게 적용
State Management 서버 상태는 TanStack Query의 캐싱·프리패칭으로 관리하고, 전역 UI 상태(인증·모달)는 Zustand로 보일러플레이트 없이 간결하게 관리
Forms React Hook Form으로 불필요한 리렌더링을 줄이고, Zod 스키마로 유효성 검사 로직을 UI와 분리해 재사용
Animation 선언형 initial/animate/exit API로 페이지 전환·모달 애니메이션을 일관된 패턴으로 구현
Charts React 친화적인 컴포넌트 기반 차트 라이브러리로 관리자 대시보드 통계를 빠르게 시각화
Formatting 코드 스타일을 통일하고 잠재적 오류를 사전에 방지해 협업 시 리뷰 비용을 줄임
Package Manager Next.js 생태계와의 호환성과 안정적인 패키지 관리

📂 핵심 구조

src/
├── app/                         # 페이지 (Next.js App Router)
│   ├── (home)/                  # 메인 홈화면
│   ├── event/                   # 행사 (upcoming / ended / booking)
│   ├── my/                      # 마이페이지 (tickets / party-history / experience-posts)
│   ├── admin_dashboard/         # 관리자 대시보드 (웹뷰)
│   ├── community/               # 커뮤니티
│   ├── challenge/               # 챌린지
│   └── exchange/                # 교환
└── shared/                      # 전역 공통 모듈
    ├── api/                     # Axios 인스턴스 (public / private / internal)
    ├── components/              # 공통 컴포넌트 (Header, SideBar, Pagination, Icon …)
    ├── stores/                  # Zustand 전역 상태 (useAuthStore, useModalStore …)
    ├── providers/               # React 컨텍스트 프로바이더 (AuthBootstrap 포함)
    ├── hooks/                   # 공통 훅
    └── types/                   # 전역 타입 정의

각 페이지 디렉터리 내부에 (api) · (component) · (hook) · (util) 폴더를 두어 페이지 전용 로직과 shared의 재사용 코드를 엄격히 분리합니다. 기능이 추가될 때 영향 범위가 해당 페이지 폴더 내로 한정되어 유지보수가 용이합니다.


🙋🏻 ReFit Lab의 FE Developer를 소개합니다!

조성민 박태경

조성민 @seongmin36

프로젝트 초기 세팅부터 유저·관리자 페이지, 그리고 전역 인증·프리패칭 같은 인프라 레이어까지 폭넓게 담당했습니다. 특히 Vite+React SPA와 달리 Next.js App Router 환경에서 SSR hydration·프리패칭·React Compiler를 실무 수준으로 다루는 것을 목표로, 그 과정에서 마주친 구조적 문제와 해결을 각 섹션에 정리했습니다.


프로젝트 기반 설정 & Icon 컴포넌트 시스템

@svgr/webpack clsx tailwind-merge

팀 개발의 기초 공사에 해당하는 파트입니다. 이슈·PR 템플릿, 조건부 스타일링 유틸(clsx + tailwind-merge), SVG 아이콘 시스템까지를 초기에 세팅해 이후 모든 기능 개발이 이 공통 규약 위에서 돌아가도록 토대를 만들었습니다.

Icon 컴포넌트 — 단일 진입점 vs 개별 컴포넌트

SVG 파일마다 개별 React 컴포넌트를 만드는 방식은 아이콘이 늘어날수록 임포트 경로가 파편화되고 프로젝트 전체에서 아이콘 사용 방식이 일관되지 않게 됩니다. @svgr/webpack으로 SVG를 빌드 타임에 React 컴포넌트로 변환하고, 단일 <Icon /> 진입점이 icon prop으로 SVG 컴포넌트를 주입받는 구조를 채택했습니다.

// shared/components/Icon.tsx
const Icon = ({ size, width, height, color = "currentColor", icon: Component, ...props }: IconProps) => {
  return <Component width={width ?? size} height={height ?? size} color={color} {...props} />;
};

// 사용 예 — SVG를 임포트한 뒤 icon prop으로 전달
import HeartIcon from "@/assets/icon/heart.svg";
<Icon icon={HeartIcon} size={24} color="#642C8D" />
// next.config.ts — turbopack과 webpack 양쪽에 @svgr/webpack 설정
turbopack: {
  rules: { "*.svg": { loaders: ["@svgr/webpack"], as: "*.js" } },
},
webpack: (config) => {
  // 기존 file-loader 규칙에서 SVG를 제외하고 @svgr/webpack으로 위임
  const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.(".svg"));
  config.module.rules.push({
    test: /\.svg$/i,
    issuer: fileLoaderRule.issuer,
    use: [{ loader: "@svgr/webpack", options: { typescript: true, ext: "tsx" } }],
  });
  fileLoaderRule.exclude = /\.svg$/i;
  return config;
},

교훈 — 공용 컴포넌트는 "지금 몇 개인가"가 아니라 "추가·제거·교체 비용이 얼마나 드는가"로 설계해야 한다.


파티 예약 페이지 & 이미지 갤러리 모달

Swiper TanStack Query

(#2) 파티 메인 (#6) 파티 상세 (#12) 파티 예약 (#12) 예약 완료

파티 메인·상세·예약 플로우 전체 UI와, 행사 상세의 이미지 갤러리 모달을 구현했습니다. 터치 스와이프·인디케이터·키보드 탐색까지 Swiper 기반으로 통합했습니다.

삽질 — 갤러리 모달 이미지 unmount 버그

갤러리 모달에서 이미지를 스와이프한 뒤 모달을 닫으면, 다음에 모달을 다시 열었을 때 이전 슬라이드 인덱스가 복원되지 않거나 엉뚱한 이미지가 보이는 현상이 발생했습니다.

(#6) 갤러리 모달

원인은 두 가지가 겹쳐 있었습니다.

  • Swiper는 성능상의 이유로 슬라이드를 가상 DOM으로 유지하며 내부 인덱스·트랜지션 상태를 인스턴스 내부에 계속 보관합니다.
  • 모달 컴포넌트는 isOpen에 따라 조건부 렌더링되는데, React가 unmount 시 DOM은 제거해도 Swiper 인스턴스가 들고 있던 상태까지 초기화해 주지는 않습니다.

결과적으로 "모달은 닫혔지만 Swiper 인스턴스는 살아 있는 듯 죽어 있는 듯"한 상태가 재오픈 시 엉뚱한 슬라이드를 표시하는 것이었습니다.

// Before — 모달 재오픈 시 Swiper 내부 상태 잔존
<Swiper>
  {images.map((img) => <SwiperSlide key={img.id}><img src={img.url} /></SwiperSlide>)}
</Swiper>

// After — key 변경으로 모달 오픈마다 Swiper 인스턴스 자체를 교체
<Swiper key={isOpen ? "open" : "closed"}>
  {images.map((img) => <SwiperSlide key={img.id}><img src={img.url} /></SwiperSlide>)}
</Swiper>

key 값이 바뀌면 React는 이전 컴포넌트를 unmount하고 새 인스턴스를 mount합니다. Swiper 인스턴스도 덩달아 새로 만들어지면서 내부 상태가 완전히 초기화됩니다.

교훈 — 서드파티 컴포넌트가 내부 상태를 갖는다면, React의 조건부 렌더링만으로는 상태 초기화를 보장할 수 없다. key는 상태 초기화를 강제하는 React의 공식 이스케이프 해치다.


마이페이지 전체 구현

qrcode.react Zustand TanStack Query clsx

(#16) 마이페이지 메인 (#16) 나의 티켓 리스트 (#16) QR 티켓 모달 (#16) 프로필 이미지 업로드

프로필 이미지 업로드, 파티 입장 QR 티켓, 참가한 행사 히스토리, 작성한 글 보기 — 네 가지 기능을 설계·구현했습니다.

FileUploader & ProfileImgUploader — 역할 분리

프로필 이미지 업로드만 보면 단일 컴포넌트로 충분해 보입니다. 하지만 같은 프로젝트에서 행사 등록 포스터·커뮤니티 첨부 이미지 등 "파일을 올린다"는 핵심 로직이 반복될 예정이었습니다.

  • FileUploader — 파일 선택·이미지 타입 검증·업로드 API 호출까지의 범용 책임
  • ProfileImgUploaderFileUploader를 내부에서 사용하며, 프로필 전용 UI(useRef로 숨긴 input을 버튼 클릭 시 .click() 트리거, 업로드 중 상태 표시)만 추가

이 역할 분리는 이후 행사 등록 페이지에서 FileUploader를 그대로 재사용할 때 "한 번 만든 것이 두 번 일한다"는 형태로 보상을 받았습니다.

QR 티켓 — 왜 SVG인가

qrcode.reactQRCodeSVGQRCodeCanvas 두 변형을 제공합니다. SVG를 택한 이유는 세 가지입니다.

  • 해상도 — 티켓을 확대해도 벡터이므로 픽셀 손실이 없다. 스캐너가 화면을 당겨 찍어도 인식률이 떨어지지 않는다.
  • 스크린샷 품질 — Canvas는 DPR(device pixel ratio) 영향을 크게 받는 반면, SVG는 브라우저가 해당 해상도에 맞춰 그린다.
  • 접근성<path>aria-* 속성을 직접 부여할 수 있어 스크린리더 보조 설명을 넣기 쉽다.
// TicketDetailModal.tsx — 서버가 내려준 qrCode 값을 SVG로 렌더링
<QRCodeSVG value={ticket.qrCode} size={192} level="H" />

Pagination 공용 컴포넌트화 — Controlled 패턴 선택 근거

초기에는 파티 히스토리와 작성한 글 목록이 각자의 로컬 useState로 페이지 상태를 관리하고, 버튼 렌더링 JSX도 거의 복붙 수준으로 반복되고 있었습니다.

상태를 내부에 두지 않고 currentPage · onPageChange를 외부에서 받는 Controlled Component로 설계한 이유는, 호출 페이지에 따라 URL 쿼리스트링 동기화(?page=3 공유 링크)나 TanStack Query 캐시 키 조합 등 페이지 상태의 제어 주체가 달라질 수 있기 때문입니다. totalPages <= 1에서 null 반환으로 단일 페이지 케이스의 UI 노이즈도 제거했습니다.

// shared/components/Pagination.tsx
export const Pagination = ({ currentPage, totalPages, onPageChange }: PaginationProps) => {
  if (totalPages <= 1) return null;

  return (
    <div className="flex fixed bottom-4 left-0 right-0 items-center justify-center gap-3 py-5">
      {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
        <button
          key={page}
          onClick={() => onPageChange(page)}
          className={clsx("text-sm transition-colors tracking-[-0.5px]", {
            "text-gray-5A font-semibold": currentPage === page,
            "text-gray-6 font-medium": currentPage !== page,
          })}
        >
          {page}
        </button>
      ))}
    </div>
  );
};

관리자 대시보드 — Zustand immer로 모달 상태 통합

Recharts Zustand immer useShallow React Hook Form Zod

(#20) 관리자 대시보드 메인 (#25) 행사 등록 (#25) 행사글 작성

관리자 전용 웹뷰의 전용 사이드바, Recharts 기반 통계 차트, 행사 등록 폼, 예약 현황 모달을 구현했습니다. 이 과정에서 모달 상태 관리 방식을 로컬 state 분산 → 전역 스토어 통합으로 한 차례 리팩토링했습니다.

초기 구조의 한계 — 모달 상태 분산

각 컴포넌트가 자신의 모달을 useState로 관리하는 초기 구조는 컴포넌트 수가 늘어날수록 세 가지 문제가 동시에 발생했습니다.

// Before — 페이지마다 모달 상태가 파편화
const [profileModalOpen, setProfileModalOpen] = useState(false);
const [galleryModalOpen, setGalleryModalOpen] = useState(false);
const [ticketModalOpen, setTicketModalOpen] = useState(false);
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null); // 모달 대상 데이터도 따로 관리
// 자식 컴포넌트에 setProfileModalOpen, setSelectedTicket... props drilling 필수
// 모달이 추가될 때마다 useState·핸들러·조건부 렌더링이 3세트씩 늘어남
// 여러 모달이 동시에 열리지 않도록 서로를 닫는 로직도 수동으로 조율
  • 모달 대상 데이터를 하위로 내려보내야 하는 props drilling
  • 여러 모달이 동시에 열릴 가능성을 일일이 차단하는 수동 조율 로직
  • "지금 어떤 모달이 열려 있나"를 디버깅할 때 흩어진 로컬 state를 일일이 추적해야 하는 비용

설계 — 모달 타입 유니언 + Zustand immer

모달 타입을 "profile" | "party-gallery" | "ticket-detail" | null 유니언으로 정의하고, 단일 스토어가 "현재 어떤 모달이 열려 있는지 · 어떤 데이터를 들고 있는지"를 단일 원천으로 관리하도록 리팩토링했습니다.

// shared/stores/useModalStore.ts
export const useModalStore = create<ModalState>()(
  immer((set) => ({
    isOpen: false,
    type: null,
    modalData: null,
    actions: {
      openModal: (type, data) => {
        set((state) => {
          state.isOpen = true;   // immer 덕분에 직접 변이처럼 작성, 내부에서 불변 처리
          state.type = type;
          state.modalData = data;
        });
      },
      closeModal: () => {
        set((state) => {
          state.isOpen = false;
          state.type = null;
          state.modalData = null;
        });
      },
    },
  }))
);

export const useModalInfo = () =>
  useModalStore(useShallow((s) => ({ isOpen: s.isOpen, type: s.type, modalData: s.modalData })));
export const useModalActions = () => useModalStore((s) => s.actions);

왜 immer인가 — 지금 스토어는 필드가 3개뿐이라 set({ ...state, ... }) 직접 작성으로도 충분합니다. 하지만 향후 모달 스택·백드롭 카운트·모달별 로컬 state 같은 중첩 상태가 추가되면 불변 업데이트 보일러플레이트가 급격히 늘어납니다. immer를 지금 도입해 두면 state 트리가 깊어져도 state.a.b.c = v 형태로 일관된 코드를 유지할 수 있습니다.

왜 useShallow인가useModalInfo가 반환하는 객체는 매 렌더마다 새 참조가 만들어집니다. useShallow를 걸지 않으면 내부 필드가 하나도 안 바뀌어도 Zustand가 객체 참조 차이만으로 리렌더를 트리거합니다. 반면 actions 객체는 스토어 생성 시 한 번 만들어진 뒤 참조가 고정되므로 useShallow가 불필요합니다 — 셀렉터 선택의 "기계적 일괄 적용"을 피하고 필드 성격에 맞게 최적화 전략을 분리한 지점입니다.


전역 인증 상태 관리 — useAuthStore + SSR Hydration

Zustand persist Axios Interceptor SSR Hydration

설계 원칙 — 단일 원천, 파생 훅으로 구독 범위 최소화

인증 상태를 여러 컴포넌트나 Context에 분산해두면 로그아웃 시 잔류 상태가 생길 위험이 있습니다. 스토어는 useAuthStore 하나로 두되, 소비 측은 필요에 맞는 파생 훅만 구독하게 나눴습니다.

  • useAuthInfo — 로그인 상태·토큰·유저 정보가 필요한 UI용
  • useAuthActions — 액션만 호출하는 곳(로그아웃 버튼 등)용
  • useAuth — 둘 다 필요한 곳용

이렇게 나누면 "액션만 호출하는 로그아웃 버튼"이 유저 정보 변경에 리렌더되지 않습니다. 구독 범위를 "해당 훅이 정말 필요로 하는 최소 상태"로 좁히는 것이 리렌더 최적화의 기본기입니다.

// useShallow로 참조가 바뀐 필드만 리렌더
export const useAuthInfo = () =>
  useAuthStore(useShallow((s) => ({
    accessToken: s.accessToken, user: s.user,
    isLoggedIn: s.isLoggedIn, hydrated: s.hydrated,
  })));

// actions 객체는 참조 고정 → useShallow 불필요
export const useAuthActions = () => useAuthStore((s) => s.actions);
export const useAuth = () => ({ ...useAuthInfo(), ...useAuthActions() });

삽질 — Next.js SSR hydration 불일치 (Flash of Unauthenticated Content)

Zustand persist는 클라이언트에서 localStorage를 읽어 스토어를 복원합니다. 서버에서는 localStorage가 없으므로 SSR이 "로그아웃 상태"로 HTML을 그린 뒤, 클라이언트 하이드레이션에서 "로그인 상태"로 바뀝니다. 개발 모드로 페이지를 새로고침하는 순간 콘솔에 이 경고가 반복적으로 찍혔습니다.

Warning: Hydration failed because the server rendered HTML didn't match the client.
  Server: "로그인"
  Client: "조성민님"

프로필 영역이 "로그인" 버튼으로 잠깐 번쩍인 뒤 "조성민님"으로 교체되는 Flash of Unauthenticated Content가 눈에 보이는 형태로 재현됐습니다.

hydrated 플래그를 스토어에 추가하고 onRehydrateStorage 콜백에서 복원이 끝난 시점에 true로 전환했습니다. 인증 의존 UI는 hydrated === true가 될 때까지 렌더를 보류해 서버·클라이언트 렌더 결과를 의도적으로 일치시킵니다. 커뮤니티 페이지에서도 같은 증상이 있어 동일한 패턴을 전파했습니다.

onRehydrateStorage: () => (state, error) => {
  if (error) return;

  // 레거시 localStorage 키 마이그레이션도 이 시점에 처리
  // 이유: onRehydrateStorage는 "persist가 막 복원을 끝낸 순간,
  //       그리고 어떤 UI도 아직 이 상태를 읽지 않은 순간"이라 가장 안전하다.
  const legacy = localStorage.getItem(STORAGE_KEY.legacyAccessToken);
  if (!state?.accessToken && legacy) {
    state?.actions.setAccessToken(legacy);
    localStorage.removeItem(STORAGE_KEY.legacyAccessToken);
  } else {
    state?.actions.setAccessToken(state?.accessToken ?? null);
  }

  state?.actions._setHydrated(true); // 복원 완료 신호
},

privateAPI 인터셉터 — 왜 getState()인가

React 훅은 컴포넌트 안에서만 실행할 수 있지만 Axios 인터셉터는 훅 바깥에서 실행됩니다. useAuthStore.getState()로 스토어를 직접 참조해 매 요청마다 최신 토큰을 헤더에 주입합니다. 응답 인터셉터는 401을 만나면 자동으로 로그아웃 액션을 호출해 "만료된 토큰이 화면 어디에도 살아남지 않도록" 보장합니다.

privateAPI.interceptors.request.use((config) => {
  const token = useAuthStore.getState().accessToken; // 훅 외부에서 스토어 직접 참조
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

privateAPI.interceptors.response.use(
  (res) => res,
  (error) => {
    if (error?.response?.status === 401) {
      useAuthStore.getState().actions.logout();
    }
    return Promise.reject(error);
  }
);

행사 페이지 API 연동 & AuthBootstrap 프리패칭

TanStack Query Zustand prefetchQuery

행사 목록(upcoming · ended)·상세·마이페이지 참가 이력을 TanStack Query로 연동하면서, App Router 환경에 맞는 프리패칭 전략을 정립했습니다.

문제 — 매 페이지 이동마다 유저 API 재요청

사이드바와 헤더는 인증 유저라면 어느 페이지에서든 프로필을 표시해야 합니다. 각 페이지가 마운트 시점에 useQuery로 유저 API를 처음 호출하면 초기 렌더에서 프로필 영역만 스켈레톤이 생기는 레이아웃 시프트가 반복됩니다.

해결 — AuthBootstrap 클라이언트 컴포넌트로 선행 캐싱

AuthBootstrap을 Provider 트리 최상단에 마운트합니다. useAuthStorehydrated 플래그가 true가 된 직후 — 즉 localStorage 복원이 완료되고, 로그인 상태가 확정된 시점에 — useEffect에서 prefetchQuery를 실행해 마이페이지 데이터를 미리 캐싱합니다.

// shared/providers/AuthBootstrap.tsx
export default function AuthBootstrap({ children }: { children: React.ReactNode }) {
  const qc = useQueryClient();
  const { hydrated, isLoggedIn } = useAuthInfo();

  useEffect(() => {
    if (!hydrated || !isLoggedIn) return;

    // hydration 완료 + 로그인 확정 후 마이페이지 데이터 선행 캐싱
    qc.prefetchQuery({
      queryKey: [QUERY_KEY.myPage],
      queryFn: getMyPage,
      staleTime: 1000 * 60 * 5, // 5분간 fresh 유지
    });
  }, [hydrated, isLoggedIn, qc]);

  return <>{children}</>;
}

이후 실제 마이페이지 진입 시 캐시가 이미 채워져 있어 추가 네트워크 요청 없이 즉시 렌더링됩니다. hydrated 조건을 걸어 SSR hydration mismatch 없이 클라이언트 복원 완료 후에만 실행되는 점도 핵심입니다.


React Compiler 도입 & React 19 보안 업데이트

babel-plugin-react-compiler React 19

React 19의 React Compiler를 도입하고, 동시에 React 서버 컴포넌트 보안 취약점에 대응한 v19.2.1 업데이트를 하나의 커밋 시리즈로 묶었습니다.

React Compiler — 수동 메모이제이션의 종언

컴파일러가 컴포넌트와 훅을 정적 분석해 useMemo · useCallback을 자동으로 삽입하므로, 개발자는 의존성 배열을 일일이 맞추는 작업에서 해방됩니다. Next.js에서는 next.config.tsreactCompiler: true 옵션으로 전체 활성화할 수 있습니다.

// next.config.ts
const nextConfig: NextConfig = {
  reactCompiler: true, // 전체 컴포넌트 자동 최적화
};

점진적 도입이 필요한 경우 compilationMode: 'annotation'으로 전환하고, 최적화할 컴포넌트에만 'use memo'를 선언하는 opt-in 방식도 지원합니다.

// next.config.ts — opt-in 방식
reactCompiler: { compilationMode: 'annotation' },
// 최적화할 컴포넌트에만 선언
export default function HeavyComponent() {
  'use memo'
  // 이 컴포넌트만 컴파일러가 메모이제이션
}

삽질 — react-hooks lint 규칙 재구성

reactCompiler: true로 전환 후 기존 eslint-plugin-react-hooks의 일부 규칙이 컴파일러 변환 결과와 충돌해 불필요한 경고가 쏟아졌습니다. React Compiler 환경에 맞게 hooks lint 규칙을 명시적으로 재구성해 해결했습니다.

주의 — React Compiler는 "모든 컴포넌트를 메모이제이션해야 한다"는 의미가 아닙니다. 컴파일러는 불필요한 메모이제이션을 피하는 판단까지 수행합니다. 개발자가 할 일은 rules-of-hooks를 지키고, 컴파일러가 분석하기 어려운 가변 데이터 구조를 남발하지 않는 것입니다.

About

피우다 공모전 ReFit-Lab 프론트엔드 레포지토리입니다.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • TypeScript 99.1%
  • Other 0.9%