리팩토링 2단계: 성능 최적화 (메모화)

- SymbolCard / CharacterCard를 React.memo로 감쌈
  (심볼 그리드에서 형제 카드 변경 시 불필요 리렌더 방지)
- Liberation의 computeCompletionDate() 호출을 useMemo로 감쌈
  (520회 루프가 매 렌더마다 돌던 것을 관련 state 변경 시만 실행)
- Symbol.jsx의 로컬 formatMesoKorean 중복 정의 제거 (utils import)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-04-19 11:41:17 +09:00
parent c6ac3366cc
commit f6f1e79b82
2 changed files with 13 additions and 18 deletions

View file

@ -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)

View file

@ -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 (
<div
onClick={(e) => {
@ -109,9 +99,9 @@ function CharacterCard({ char, active, onSelect, onRemove }) {
</div>
</div>
)
}
})
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 }) {
</div>
</div>
)
}
})
export default function Symbol() {
const { setFullscreen } = useLayout()