
렌더링시 기본 캔버스의 픽셀 데이터를 가져오는 로직을 수행하는 함수.
데이터를 가져오기 + canvas 태그 내용 채우기
이 두가지 역할을 수행.
const fetchCanvasData = useCallback(async (id: string | null) => {
setIsLoading(true);
setHasError(false);
const API_URL = import.meta.env.VITE_API_URL;
const url = id
? `${API_URL}/canvas/pixels?canvas_id=${id}`
: `${API_URL}/canvas/pixels`;
try {
const res = await fetch(url, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
if (!res.ok) throw new Error('잘못된 응답');
const json = await res.json();
if (!json.success) throw new Error('실패 응답');
const {
canvas_id: fetchedId,
pixels,
canvasSize: fetchedCanvasSize,
} = json.data;
setCanvasId(fetchedId);
setCanvasSize(fetchedCanvasSize);
const source = document.createElement('canvas');
source.width = fetchedCanvasSize.width;
source.height = fetchedCanvasSize.height;
const ctx = source.getContext('2d');
if (ctx) {
ctx.fillStyle = INITIAL_BACKGROUND_COLOR;
ctx.fillRect(0, 0, fetchedCanvasSize.width, fetchedCanvasSize.height);
if (Array.isArray(pixels)) {
pixels.forEach(({ x, y, color }) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, 1, 1);
});
}
}
sourceCanvasRef.current = source;
} catch (err) {
console.error('캔버스 로딩 실패', err);
setHasError(true);
} finally {
setIsLoading(false);
onLoadingChange?.(false);
setTimeout(() => setShowCanvas(true), 100);
}
}, []);
useEffect(() => {
fetchCanvasData(initialCanvasId);
}, [initialCanvasId, fetchCanvasData]);
일단 유틸함수로 미리 분리해두었기 때문에 분리하기가 편했다.
함수 동작을 위해 필요한 정보와 타입들을 정리하고
이를 interface로 만들어서 미리 정의하였다.
기존 PixelComponent에서 이를 Props로 넘겨주는 로직만 수행하고
데이터를 받는 로직과 이를 바탕으로 캔버스를 그리는 로직을 별도 파일로 분리하였다.
interface FetchCanvasDataParams {
id: string | null;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
setHasError: React.Dispatch<React.SetStateAction<boolean>>;
setCanvasId: (id: string) => void;
setCanvasSize: React.Dispatch<React.SetStateAction<{ width: number; height: number }>>;
sourceCanvasRef: React.MutableRefObject<HTMLCanvasElement>;
onLoadingChange?: (loading: boolean) => void;
setShowCanvas: React.Dispatch<React.SetStateAction<boolean>>;
INITIAL_BACKGROUND_COLOR: string;
}
export const fetchCanvasData = async ({
id,
setIsLoading,
setHasError,
setCanvasId,
setCanvasSize,
sourceCanvasRef,
onLoadingChange,
setShowCanvas,
INITIAL_BACKGROUND_COLOR,
}: FetchCanvasDataParams) => {
setIsLoading(true);
setHasError(false);
const API_URL = import.meta.env.VITE_API_URL;
const url = id
? `${API_URL}/canvas/pixels?canvas_id=${id}`
: `${API_URL}/canvas/pixels`;
try {
const res = await fetch(url, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
if (!res.ok) throw new Error('잘못된 응답');
const json = await res.json();
if (!json.success) throw new Error('실패 응답');
const {
canvas_id: fetchedId,
pixels,
canvasSize: fetchedCanvasSize,
} = json.data;
setCanvasId(fetchedId);
setCanvasSize(fetchedCanvasSize);
const source = document.createElement('canvas');
source.width = fetchedCanvasSize.width;
source.height = fetchedCanvasSize.height;
const ctx = source.getContext('2d');
if (ctx) {
ctx.fillStyle = INITIAL_BACKGROUND_COLOR;
ctx.fillRect(0, 0, fetchedCanvasSize.width, fetchedCanvasSize.height);
if (Array.isArray(pixels)) {
pixels.forEach(({ x, y, color }: { x: number; y: number; color: string }) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, 1, 1);
});
}
}
sourceCanvasRef.current = source;
} catch (err) {
console.error('캔버스 로딩 실패', err);
setHasError(true);
} finally {
setIsLoading(false);
onLoadingChange?.(false);
setTimeout(() => setShowCanvas(true), 100);
}
};
// fetchCanvasData 분리
useEffect(() => {
fetchCanvasDataUtil({
id: initialCanvasId,
setIsLoading,
setHasError,
setCanvasId,
setCanvasSize,
sourceCanvasRef,
onLoadingChange,
setShowCanvas,
INITIAL_BACKGROUND_COLOR,
});
}, [
initialCanvasId,
setCanvasId,
setCanvasSize,
setIsLoading,
setHasError,
onLoadingChange,
setShowCanvas,
]);
import {
INITIAL_POSITION,
MIN_SCALE,
MAX_SCALE,
INITIAL_BACKGROUND_COLOR,
VIEWPORT_BACKGROUND_COLOR,
COLORS,
} from './canvasConstants';
기존에 많은 줄을 차지하고 있던 상수 값들을 canvasConstants.tsx로 분리하였다.
기존에 useState로 관리하던 현재 마우스 좌표, 캔버스 내 좌표 값등을 canvasUiStore
라는 Store를 만들고 관리하기 시작하였다.
https://zustand.docs.pmnd.rs/guides/prevent-rerenders-with-use-shallow
공식 문서의 useShallow에 관한 부분에서
구조분해할당으로 가져오면 전체 객체를 가져오는 것이기때문에
객체의 요소 중 하나만 바뀌어도 다른 요소들이 리렌더링 된다고 한다.
const {
color,
setColor,
hoverPos,
setHoverPos,
cooldown,
setCooldown,
timeLeft,
//....
isLoading,
setIsLoading,
hasError,
setHasError,
showCanvas,
setShowCanvas,
} = useCanvasUiStore();
이런식으로 가져 오는 것 보단 useShallow 함수를 활용해서 가져와야
불필요한 리렌더링을 방지 할 수 있다고 한다.
그러나 한 저장소에 저장해놓은 값들이 많은지라 오히려 더 코드의 가독성을 해친다고 판단.
그냥 개별적으로 각각 상태를 가져오기로 하였다.
const color = useCanvasUiStore((state) => state.color);
const setColor = useCanvasUiStore((state) => state.setColor);
//...
const showPalette = useCanvasUiStore((state) => state.showPalette);
const setShowPalette = useCanvasUiStore((state) => state.setShowPalette);
PixelCanvas 컴포넌트 안에 쿨다운 초를 계산해서 렌더링하는 로직이 있었다.
const startCooldown = useCallback((seconds: number) => {
setCooldown(true);
setTimeLeft(seconds);
const timer = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
clearInterval(timer);
setCooldown(false);
return 0;
}
return prev - 1;
});
}, 1000);
}, []);
현재 잔여 시간 관련한 상태를 Zustand로 빼놓은 상태라서
const timeLeft = useCanvasUiStore((state) => state.timeLeft);
const setTimeLeft = useCanvasUiStore((state) => state.setTimeLeft);
남은 쿨다운 잔여 시간을 계산하는 로직을 Zustand 저장소의 action 으로 두어
로직과 상태를 함께 두었다.
startCooldown: (seconds: number) => {
if (get().cooldown) return;
set({ cooldown: true, timeLeft: seconds });
const timer = setInterval(() => {
const newTimeLeft = get().timeLeft - 1;
if (newTimeLeft <= 0) {
clearInterval(timer);
set({ cooldown: false, timeLeft: 0 });
} else {
set({ timeLeft: newTimeLeft });
}
}, 1000);
},
모바일 이벤트 핸들러가 또 들어가면서 컴포넌트가 정말 무거워졌다...
이거 얼른 정리하자...ㅠ