From f6f1e79b8217806bbfea6f6bebec3bbadf4d9768 Mon Sep 17 00:00:00 2001 From: caadiq Date: Sun, 19 Apr 2026 11:41:17 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=202?= =?UTF-8?q?=EB=8B=A8=EA=B3=84:=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94=20(=EB=A9=94=EB=AA=A8=ED=99=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SymbolCard / CharacterCard를 React.memo로 감쌈 (심볼 그리드에서 형제 카드 변경 시 불필요 리렌더 방지) - Liberation의 computeCompletionDate() 호출을 useMemo로 감쌈 (520회 루프가 매 렌더마다 돌던 것을 관련 state 변경 시만 실행) - Symbol.jsx의 로컬 formatMesoKorean 중복 정의 제거 (utils import) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/features/liberation/pc/Liberation.jsx | 9 ++++++-- frontend/src/features/symbol/pc/Symbol.jsx | 22 +++++-------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/frontend/src/features/liberation/pc/Liberation.jsx b/frontend/src/features/liberation/pc/Liberation.jsx index 32f28c2..f6032c4 100644 --- a/frontend/src/features/liberation/pc/Liberation.jsx +++ b/frontend/src/features/liberation/pc/Liberation.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useLayoutEffect } from 'react' +import { useState, useEffect, useLayoutEffect, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' import { api } from '../../../api/client' @@ -261,7 +261,12 @@ export default function Liberation() { return { start: ws, end: ws.add(6, 'day') } } - const completionDate = computeCompletionDate() + const completionDate = useMemo( + () => computeCompletionDate(), + // 의도적으로 state 전체 + calcMode 만 의존. 내부 함수는 클로저 안의 값만 읽음 + // eslint-disable-next-line react-hooks/exhaustive-deps + [state, calcMode, alreadyDone, remaining, weeklyEarn, doneEarn, monthlyEarn, monthlyDoneThisMonth], + ) const isDone = completionDate !== null const [resetOpen, setResetOpen] = useState(false) diff --git a/frontend/src/features/symbol/pc/Symbol.jsx b/frontend/src/features/symbol/pc/Symbol.jsx index 36d7bd7..9b57089 100644 --- a/frontend/src/features/symbol/pc/Symbol.jsx +++ b/frontend/src/features/symbol/pc/Symbol.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useLayoutEffect, useMemo } from 'react' +import { memo, useState, useEffect, useLayoutEffect, useMemo } from 'react' import { useQuery, useQueries, useMutation } from '@tanstack/react-query' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' @@ -9,6 +9,7 @@ import Select from '../../../components/common/Select' import Tooltip from '../../../components/common/Tooltip' import CharacterSuggestDropdown from '../../../components/common/CharacterSuggestDropdown' import { useSymbolStore } from '../store' +import { formatMesoKorean } from '../../../utils/formatting' dayjs.extend(utc) dayjs.extend(timezone) @@ -44,20 +45,9 @@ function computeCompletion({ remainingSymbols, daily, weeklyPerWeek, extra, dail return { days: null, date: null } } -function formatMesoKorean(n) { - const v = Number(n) || 0 - if (v <= 0) return '0' - const eok = Math.floor(v / 100_000_000) - const man = Math.floor((v % 100_000_000) / 10_000) - const parts = [] - if (eok) parts.push(`${eok.toLocaleString()}억`) - if (man) parts.push(`${man.toLocaleString()}만`) - return parts.length ? parts.join(' ') : v.toLocaleString() -} - const TYPE_ORDER = ['아케인', '어센틱', '그랜드 어센틱'] -function CharacterCard({ char, active, onSelect, onRemove }) { +const CharacterCard = memo(function CharacterCard({ char, active, onSelect, onRemove }) { return (
{ @@ -109,9 +99,9 @@ function CharacterCard({ char, active, onSelect, onRemove }) {
) -} +}) -function SymbolCard({ symbol, equipped, charId }) { +const SymbolCard = memo(function SymbolCard({ symbol, equipped, charId }) { const progress = useSymbolStore((s) => s.progress?.[charId]?.[symbol.id]) const updateSymbol = useSymbolStore((s) => s.updateSymbol) @@ -367,7 +357,7 @@ function SymbolCard({ symbol, equipped, charId }) { ) -} +}) export default function Symbol() { const { setFullscreen } = useLayout()