New Name์ Text, SVG, Image, GIF๋ฅผ ์ด์ฉํด ์ฝ๊ฒ ์ธ๋กํ ๋ช
ํจ์ ์์ฑํ ์ ์๋ ๋ชจ๋ฐ์ผ ์ดํ๋ฆฌ์ผ์ด์
์
๋๋ค.
๋ช ํจ์ ์์ฑํ๊ณ ์ด๋ฏธ์ง ํน์ GIF๋ก ์ ์ฅํ์ฌ ์์ฝ๊ฒ ๋ช ํจ์ ์ ๋ฌํ ์ ์์ต๋๋ค.
์ด๋ฒ ํ๋ก์ ํธ์ ๋ชฉํ๋ ๋ชจ๋ฐ์ผ์ด๋ผ๋ ํน์ฑ์ ๋ง์ฐ์ค๊ฐ ์๋ ์๊ฐ๋ฝ์ ์ด์ฉํด GUI ์ ์ด์ฉํ ์ ์๋ ๊ธฐ๋ฅ์ด ์๋ ๋ชจ๋ฐ์ผ ์ดํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค์ด๋ณด๊ณ ์ ํ์์ต๋๋ค.
์ด ๋ชฉํ์ ์ ํฉํ ์์ด๋์ด๋ฅผ ๊ณ ๋ฏผํ๋ค ์ํ๋ ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๋ ๋ชจ๋ฐ์ผ ์ด๋ฏธ์ง ์๋ํฐ ํด์ ๊ตฌํํ ๊ฒฝ์ฐ ๊ณต๋ถํ ์์๊ฐ ๋ง์ ๊ฒ์ด๋ผ ํ๋จํ์์ต๋๋ค.
์ด๋ฏธ์ง๋ผ๋ ๋ฒ์๋ฅผ ํธ๋ํฐ ํ๋ฉด ํฌ๊ธฐ์ ๋ง๋ ์ด๋ฏธ์ง๋ก ํ์ ์ง๊ณ ์ ํ์๊ณ ๋ช ํจ์ด๋ผ๋ ์ปจํ ์ธ ๊ฐ ์๊ฐ๋ฌ์ต๋๋ค.
ํ๋์ ๋ช ํจ์ ์ค๋ฌผ์ ๊ฐ์ง๊ณ ๋ค๋๊ณผ ๋์์ ๋์งํธ๋ก ๋ณด๊ด์ ํ๋ ํํ๋ฅผ ์ทจํ๊ณ ์์ต๋๋ค. ์ด๋ฅผ ์ ์ ๊ฐ ์ฌ์ฉํ๋ ์ ์ฅ์์ ์๊ฐ์ ํด๋ณด๋ฉฐ, ์ค๋ฌผ์ ์ค์บํ๋ ๊ฒ์ด ์๋ ์ฒ์๋ถํฐ ๋ช ํจ์ ๋ฐ์ดํฐ ํํ๋ค๋ก๋ง ๊ฐ๊ณ ๋ค๋ ์๋ ์์ง ์์๊น? ๋ผ๋ ๊ฐ์ ์ ํด๋ณด์์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด ๋ช ํจ์ด๋ผ๋ ์์๋ ์์ ์ ์์ด๋ดํฐํฐ๋ฅผ ๋ํ๋ด๋ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์๋๋ฐ, ์ด๋ฅผ ์ข ๋ ๋์งํธ ์์์ ํจ๊ณผ์ ์ผ๋ก ์ ๋ชฉ์ด ๊ฐ๋ฅํ GIF ๊ฐ์ ๋์ ์ด๋ฏธ์ง๊ฐ ๋ค์ด๊ฐ๋ค๋ฉด ์ฒ์ ๋ฐ์์ ๋ ๋ ์ฌ๋ฐ๊ฒ ์ ๊ทผํ ์ ์์ ๊ฒ์ด๋ผ ์๊ฐํด ์์ํ๊ฒ ๋์์ต๋๋ค.
ํ๋ก์ ํธ ์งํ ์ค ์ ์ผ ๋จผ์ ๊ณ ๋ คํ๋ ๋ถ๋ถ์ ํฐ์น์์๋ฅผ ์ด๋ป๊ฒ ๋ด๊ฐ ์ํ๋ ๋ฐฉ์์ผ๋ก ์ปจํธ๋กค ํ ์ง ์์ต๋๋ค.
๋ชจ๋ฐ์ผ ์ดํ๋ฆฌ์ผ์ด์ ์ด๋ผ๋ ํน์ฑ์ ์๊ฐ๋ฝ์ ์ ์ค์ฒ๋ฅผ ์ด์ฉํด ์์๋ฅผ ์ํ๋ ๋๋ก ๋ค๋ฃจ๋ ๊ฒ์ด ๊ฐ์ฅ ์ค์ํ ์์๋ผ๊ณ ์๊ฐ์ด ๋์์ต๋๋ค.
์ด๋ฌํ ์ ์ค์ฒ๋ฅผ ๋ค๋ฃจ๋ ๊ธฐ๋ฅ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ํด๊ฒฐํ๋ฉด ์์กด์ฑ์ด ๋์์ง ๊ฒ์ด๋ผ ํ๋จํ์์ต๋๋ค.
์ด์ RN์ ๋ด์ฅ API์ธ PanResponder๋ฅผ ์ด์ฉํด ๋ฉํฐ ํฐ์น๋ฅผ ํ๋์ ์ก์
์ผ๋ก ๋ฐ๊ฟ ์ ์๋ ๊ธฐ๋ฅ์ ๋ํด ๊ทผ๋ณธ์ ์ผ๋ก ์์๊ฐ๋ณด๊ณ ์ ํ์์ต๋๋ค.
PanResponder๋ RN์ ๋ผ์ดํ ์ฌ์ดํด์ ๊ด๋ฆฌํ๋ Gesture Responder System์ ์์ ํฐ์น์ ์์ธก์ ๊ฐ๋ฅํ๊ฒ ํด์ค๋๋ค.
์ฒ์ PanResponder ์ธ์คํด์ค๋ฅผ ์์ฑํ๊ธฐ ์ํด์๋ PanResponder ๋ด๋ถ์ ์ ์ ๋ฉ์๋์ธ PanResponder.create๋ฅผ ํตํด ์๋ก์ด PanResponder ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
์ด๋ฅผ RN์์์๋ useRef ํน์ useState๋ฅผ ์ด์ฉํด ์์ฑ๋ ์ธ์คํด์ค์ ์ํ๋ฅผ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
์ฌ๊ธฐ์์ useRef๋ฅผ ์ด์ฉํ์ง ์๊ณ useEffect์ useState๋ฅผ ์ด์ฉํ์๋๋ฐ ๊ทธ ์ด์ ๋ useRef๋ฅผ ์ด์ฉํ ๊ฒฝ์ฐ ๊ฐ์ด ๋ณ๊ฒฝ๋์ด๋ ์ฌ๋ ๋๋ง์ด ๋์ง ์๊ณ ๊ฐ์ ์ฐธ์กฐํ ์ ์์ต๋๋ค.
ํ์ง๋ง ์ด๋ฏธ์ง๋ GIF ๋๋ ๋ค๋ฅธ ์์๋ค์ ํฌ๊ธฐ๋ฅผ ๋ณ๊ฒฝํ๋ ์์ ์ ๋ฆฌํ์ธํ ์ด ๋ค์ ๋์ด์ผ ํ๊ธฐ ๋๋ฌธ์ ํ์๋ฅผ ์ด์ฉํ๋ ๊ฒ์ด ๋์ ๋ฐฉ๋ฒ์ด๋ผ ํ๋จํ์์ต๋๋ค.
RN์ ๊ฒฝ์ฐ UI๊ตฌ์ถ์ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ๊ตฌ์ฑ์์๋ก์จ ์ฌ์ฉ๋๋ View๋ผ๋ ํํ๋ฅผ ์ฌ์ฉํด ์ํ๋ UI๋ฅผ ํ์ํ๋ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค.
์ด View๋ฅผ PanResponder๋ฅผ ์ด์ฉํด ์์๋ฅผ ๋ถ๋๋ฝ๊ฒ ์กฐ์ ํ๋ ค๋ฉด Animated๋ผ๋ RN์ ๋ด์ฅ API๋ฅผ ๊ฐ์ด ์ด์ฉํด Animated.View๋ก ์ด์ฉํด ์ฃผ์ด์ผ ํฉ๋๋ค.
์ด๋ฅผ ์ํด ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํ ๊ฐ์ธ Animated.Value๋ฅผ ์ด์ฉํฉ๋๋ค.
์ถ๊ฐ์ ์ผ๋ก RN์์์๋ ์ด๋ค View๋ฅผ ํฐ์นํ ๊ฒฝ์ฐ ์ด๋ฒคํธ ๋ฒ๋ธ๋ง์ด ๋ฐ์ํด ์ ์ค์ฒ์ ๋์ํ๋ ์ด๋ฒคํธ ํธ๋ค๋ง์ด ๊ฐ์ฅ ์๋ View๋ก ๋ถํฐ ์์ํฉ๋๋ค.
์ด๋ฅผ ์ ์ค์ฒ ์ปจํธ๋กค์ด ๋ด๋ถ์ ์์ View๋ถํฐ ์ด๋ฒคํธ๋ฅผ ์ปจํธ๋กค ํ๋ ๊ฒ์ ์์น ์๋๋ค๋ฉด PanResponder์ onStartShouldSetPanResponder ์์ฑ์ true๋ก ๋ง๋ค์ด์ค์ผ ํฉ๋๋ค.
onStartShouldSetPanResponder: (evt, gestureState) => true,๋ค์์ onPanResponder์ ์์ฑ์ ๋๋ค.
onPanResponderGrant:PanResponder์ ์๊ฐ๋ฝ์ด ๋ฟ๊ณ ๋์ ์ฒ์ ์์ง์ผ ์์ ์ด๋ฒคํธ๋ฅผ ์ก์ ์ผ๋ก ๋ฐ๊ฟ ์ ์๋ ์์ฑ์ ๋๋ค. ์ด์ ์Animated.Value๋ฅผ ์ด๊ธฐํ์์ผ์ฃผ์ด์ผ ๊ทธ ์ ์ ์ฐธ์กฐ ๊ฐ์ ์ฐธ์กฐํ์ง ์์ต๋๋ค. ์ฒ์ ์์ง์ ์ ์ ํ๋ ์์์ ์ขํ์ ๊ฐ์ ์ด๊ธฐํ ์์ผ์ค ๋ค ์ฒซ๋ฒ์งธ ์ธ์์ธevent์gestureState๊ฐ์ฒด๋ฅผ ์ด์ฉํด ์๊ฐ๋ฝ์ ์์น๋ฅผ ์ป์ ์ ์์ต๋๋ค. ์ด ํ ์ด๋ฅผ ์ด์ฉํด ์๊ฐ๋ฝ์ด ์ด๋ํ ๊ฑฐ๋ฆฌ๋ฅผ ์ด์ฉํด ์์์ ํฌ๊ธฐ ํน์ ์ขํ๋ฅผ ๋ณ๊ฒฝํ์์ต๋๋ค.onPanResponderMove: Grant๋ ์ฒซ ์์ง์ ์ ๋ง์ ๊ฐ์ ์ฐธ์กฐํ๊ธฐ ๋๋ฌธ์ ๊ทธ ํ์ ๊ฐ์ onPanResponderMove๋ฅผ ์ด์ฉํด ๊ฐ์ ์กฐ์ ํ ์ ์์ต๋๋ค. ์ด๋ ์๊ฐ๋ฝ์ด ์ฒซ ์์ง์์ด ์๊ธด ํ ๋ผ์ง๊ธฐ ์ ๊น์ง ์ผ๋ จ์ ์ฐ์์ ์ธ ์ ์ค์ฒ๋ฅผ ์ก์ ์ผ๋ก ์ปจํธ๋กค ํฉ๋๋ค.onPanResponderRelease: ์๊ฐ๋ฝ์ด ๋จ์ด์ง๋ ์๊ฐ ์ก์ ์ ์ ์ํ ์ ์์ต๋๋ค. ๊ฐ์ ๋ฐ์ ์์ผ์ค ํAnimated.Value๋ฅผ ๋ค์ ์ด๊ธฐํ ์์ผ์ฃผ์ด์ผ ํฉ๋๋ค.
์๋๋ ์์ ์ฝ๋์ ๋๋ค.
useEffect(() => {
setMoveResponder(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: (_, gestureState) => {
position = {
x: gestureState.dx,
y: gestureState.dy,
};
movePan.setOffset(position);
movePan.setValue({ x: 0, y: 0 });
},
onPanResponderMove: Animated.event(
[null, { dx: movePan.x, dy: movePan.y }],
{ useNativeDriver: false },
),
onPanResponderRelease: ({ nativeEvent }, gestureState) => {
if (selectedIndexRef.current === null) return;
positionRef = {
x: positionRef.x + gestureState.dx,
y: positionRef.y + gestureState.dy,
};
dispatch(
updateImagePosition({
index: selectedIndexRef.current,
x: position.x,
y: position.y,
}),
);
movePan.setValue({ x: 0, y: 0 });
},
}),
);
}, [movePan]);์์์ ํฐ์น๋ฅผ ํ ๊ฒฝ์ฐ ์ํ๋ ๊ฐ ๋งํผ ์ด๋ํ์ง ์๊ณ ์์๋๋ ๊ฐ์ด ์๋ ๋ค๋ฅธ ๊ฐ์ ์ป๋ ์ด์๊ฐ ๋ฐ์ํ์์ต๋๋ค.
PanResponder๋ฅผ ์ด์ฉํ ๊ฒฝ์ฐ useState์ ๊ฒฝ์ฐ batch๋ผ๋ ์ฌ๋ฌ ์ํ ์
๋ฐ์ดํธ๋ฅผ ์ผ๊ด ์ฒ๋ฆฌํ๋ ์์ฑ์ ์ด์ฉํด ์คํ์ด ๋ฉ๋๋ค.
์ด๋ก ์ธํด ํฐ์น๊ฐ ๋ ๊ฒฝ์ฐ, ํฐ์น๊ฐ ๋ฐ๋ก ๋๋ฆฐ ํ์ฌ์ ๊ฐ๋ง ์ด์ฉํ ์ ์์๊ณ PanResponder ์ธ์คํด์ค ์์ useState๋ก ๊ด๋ฆฌํ ๊ฐ์ ๊ฒฝ์ฐ ์ฆ๊ฐ์ ์ผ๋ก ํผ๋๋ฐฑ๋์ด ๋ฐ์๋ ์ ์์์ต๋๋ค.
๊ทธ๋์ useRef๋ฅผ ์ด์ฉํ์์ต๋๋ค. useRef๋ ํญ์ ์ต์ ์ ์ํ๋ฅผ ์ฐธ์กฐํ ์ ์์๊ธฐ์ ํด๊ฒฐ ํ ์ ์์์ต๋๋ค.
positionRef.current = {
x: gestureState.dx,
y: gestureState.dy,
};์ด๋์ ๋ค๋ฃจ๋ ๊ฒ์ ํ๋์ ์๊ฐ๋ฝ์ด ์ด๋ํ ๊ฑฐ๋ฆฌ๋งํผ์ useRef๋ฅผ ์ด์ฉํด ๋ณ๊ฒฝํด์ค ์ ์์๋๋ฐ, ์ฌ์ด์ฆ๋ฅผ ์กฐ์ ํ๋ ๊ฒ์ ์ด๋ป๊ฒ ํด์ผํ ๊น? ๋ผ๋ ๊ณ ๋ฏผ์ ๋ง๋ฅ๋จ๋ฆฌ๊ฒ ๋์์ต๋๋ค.
์ด์ ์ฌ์ฉ์๊ฐ ์น์ํ๊ฒ ๋๋ ์ ์๋ ์ฌ์ด์ฆ ์กฐ์ ๋ฐฉ๋ฒ์ ๋๊ฐ์ง ์๊ฐํด๋ณด๊ฒ ๋์์ต๋๋ค.
- ๋๊ฐ์ ์๊ฐ๋ฝ์ ์ด์ฉํด ํ์น ์ค,์์์ ์ด์ฉํ๋ ๋ฐฉ๋ฒ
- ์ฌ๋ผ์ด๋ ์คํฌ๋กค์ ์ด์ฉํด ์ ๋ฐํ๊ฒ ์กฐ์ ํ๋ ๋ฐฉ๋ฒ
์ ๋ ์ฒ์ ๋ฐฉ๋ฒ์ธ ๋ ์๊ฐ๋ฝ์ ๊ฑฐ๋ฆฌ๊ฐ ์ฒ์ ๋ฟ์์ ๋๋ฅผ ๊ตฌํ๋ ๊ฒ๋ถํฐ ์์์ ํด๋ณด์๋ผ๊ณ ์๊ฐ์ ํ์์ต๋๋ค.
- ์ฒซ๋ฒ์งธ ์ด๋ฒคํธ ์ธ์์์
nativeEvent๋ฅผ ๊ตฌ์กฐ๋ถํดํ ๋น์ ์ด์ฉํด ๋ด๋ถ์ ๊ฐ๋ค์ ๊ฐ์ ธ์จ ๋ค ๊ทธ ์์์touches๋ผ๋ ํ์ฌ ๋ฟ์ ์๋ ํฐ์น๋ฅผ ๊ตฌํ ์ ์๋ ์์ฑ์ ์ด์ฉํด ํ์ฌtouches๋ฐฐ์ด์ ๊ธธ์ด๋ฅผ ์ด์ฉํด, ์๊ฐ๋ฝ์ด ๋๊ฐ๊ฐ ๋ฟ์ง ์์์ ๋๋early returnํ๋ ์์ผ๋ก ์ฒ์ ์์์ ์ก์์ต๋๋ค. - ์ฌ๊ธฐ์์ ๋ ์๊ฐ๋ฝ ์ฌ์ด์ ๊ฑฐ๋ฆฌ๋ฅผ ๊ตฌํ๋
getDistanceํจ์๋ฅผ ํํ ํ ๋ค ํผํ๊ณ ๋ผ์ค์ ์ ๋ฆฌ๋ฅผ ์ด์ฉํด ๊ตฌํด๋ณด์์ต๋๋ค.
const getDistance = (touch1, touch2) => {
const dx = touch1.pageX - touch2.pageX;
const dy = touch1.pageY - touch2.pageY;
return Math.sqrt(dx * dx + dy * dy);
}; onPanResponderGrant: ({ nativeEvent }) => {
const { touches } = nativeEvent;
if (selectedIndexRef.current === null || touches.length !== 2) return;
if (touches.length === 2) {
const [touch1, touch2] = touches;
const distance = getDistance(touch1, touch2);
sizePositionRef.current = {
xy: distance,
};
scaleRefXY._startingDistance = distance;
}
},onPanResponderGrant์distance๋ฅผ ์ด๊ธฐ ๊ฐ์ผ๋ก ์ก์ ์ค ๋ค ๊ทธ ๊ฐ์ ์ด๋ ์ค์ผ ๋ ๋น๊ตํด ์ฃผ๋ ค ํ์์ต๋๋ค.
onPanResponderMove: ({ nativeEvent }) => {
const { touches } = nativeEvent;
if (selectedIndexRef.current === null || touches.length !== 2) return;
if (touches.length === 2) {
const [touch1, touch2] = touches;
const distance = getDistance(touch1, touch2);
const scaleFactorAll = distance / scaleRefXY._startingDistance;
scaleRefXY.setValue(scaleFactorAll);
}
},onPanResponderMove๋์ค์distance๋ฅผonPanResponderGrant์distance์ ๋น๊ตํดscaleFactor๋ฅผ ๊ณ์ฐํด์ฃผ์์ต๋๋ค.
return (
<Animated.View
key={element[index]?.id}
style={[
{ position: "absolute" },
positionStyle,
{ transform: [{ scale: scaleRefXY }] },
]}
onPress={() => handleSelect(index)}
{...resizeResponder.panHandlers}
>
<TouchableOpacity>{shapeElements}</TouchableOpacity>
</Animated.View>
);- ์ด๋ฅผ ์ด์ฉํด
Animate.View์scale์์ฑ์scaleRef๋ฅผ ์ด์ฉํดtransformํด์ฃผ์์ต๋๋ค.
๊ทธ ์ดํ๋ X์ถ๊ณผ Y์ถ์ scale์ ๊ฐ๊ฐ ์กฐ์ ํ ์๋ ์์๊น? ๋ผ๋ ๊ณ ๋ฏผ์ ํด๋ณด๊ฒ ๋์์ต๋๋ค.
- ์ด๋ฒ์๋ ํผํ๊ณ ๋ผ์ค์ ์ ๋ฆฌ๋ฅผ ์ด์ฉํ๋ ๊ฒ์ด ์๋ ์๊ฐ๋ฝ์ X์ถ ์ด๋ ๋ฒ์์ Y์ถ ์ด๋ ๋ฒ์์ค ํฐ ๋ถ๋ถ์ด ์ค์ฌ์ด ๋์ด ์ด๋ํ๋ฉด ๋๊ฒ ๋ค ๋ผ๊ณ ์๊ฐํ์์ต๋๋ค.
onPanResponderGrant: ({ nativeEvent }) => {
const { touches } = nativeEvent;
if (selectedIndexRef.current === null || touches.length !== 2) return;
if (touches.length === 2) {
const [touch1, touch2] = touches;
const distance = getDistance(touch1, touch2);
const distanceX = Math.abs(touch1.pageX - touch2.pageX);
const distanceY = Math.abs(touch1.pageY - touch2.pageY);
sizePositionRef.current = {
xy: distance,
x: distanceX,
y: distanceY,
};
scaleRef._startingDistance = {
x: distanceX,
y: distanceY,
};
scaleRefXY._startingDistance = distance;
}
},
onPanResponderMove: ({ nativeEvent }) => {
const { touches } = nativeEvent;
if (selectedIndexRef.current === null || touches.length !== 2) return;
if (touches.length === 2) {
const [touch1, touch2] = touches;
const distance = getDistance(touch1, touch2);
const distanceX = Math.abs(touch1.pageX - touch2.pageX);
const distanceY = Math.abs(touch1.pageY - touch2.pageY);
const scaleFactorAll = distance / scaleRefXY._startingDistance;
const scaleFactorX = distanceX / scaleRef._startingDistance.x;
const scaleFactorY = distanceY / scaleRef._startingDistance.y;
scaleRefXY.setValue(scaleFactorAll);
if (scaleFactorX > scaleFactorY && !sizeProportionMode) {
scaleRef.setValue({
x: scaleFactorX,
y: 1,
});
}
if (scaleFactorX < scaleFactorY && !sizeProportionMode) {
scaleRef.setValue({
x: 1,
y: scaleFactorY,
});
}
}
},if (sizeProportionMode) {
transform.push({ scale: scaleRefXY });
} else {
transform.push({ scaleX: scaleRef.x });
transform.push({ scaleY: scaleRef.y });
}
return (
<Animated.View
key={element[index]?.id}
onPress={() => handleSelect(index)}
style={[{ position: "absolute" }, positionStyle, { transform }]}
{...resizeResponder.panHandlers}
>
<TouchableOpacity>{shapeElements}</TouchableOpacity>
</Animated.View>
);sizeProportionMode๋ฅผ ์ค์ ํ ๋ค ๋น๋ก ๋ชจ๋๊ฐ ์๋ ๋๋X์Y์scaleFactor๋ฅผ ๊ฑฐ๋ฆฌ์ ์ฐจ๋ฅผ ์ด์ฉํด ๊ตฌํด์ฃผ์์ต๋๋ค.
์ด๋ ๊ฒ ์์ ๊ณ ๋ฏผ๋ค์ ์ง๋์ณ ์ค๋ฉฐ ์ด๋ฏธ์ง์ ์์๋ค์ ํฐ์น๋ฅผ ์ด์ฉํด ์กฐ์ ํ๋ ๋ฐฉ๋ฒ์ ๊ฐ๊ตฌํ์์ผ๋, ์ด์ ๊ฐ๊ฐ์ ๊ฐ์ ๊ฐ๋ณ์ ์ผ๋ก ์ ์ฉ์์ผ์ฃผ๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
์ถ๊ฐ์ ์ธ ๋ฌธ์ ๋ ๋ง์ ์์์ ์ํ๋ฅผ ์ผ๋ จ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ์์ ์ด ๋ฌธ์ ์์ต๋๋ค.
์์ธก์ด ๊ฐ๋ฅํ๊ฒํ๊ณ , ๋๋ฒ๊น
์ ์ฉ์ดํ๊ฒ ๊ด๋ฆฌํ๊ธฐ ์ํด Redux๋ฅผ ์ด์ฉํ ์ ์ญ ์ํ ๊ด๋ฆฌ๋ฅผ ์ ํํ์์ต๋๋ค.
ํ๋์ ํ๋ฉด์ ์ฌ๋ฌ ์์๋ฅผ ๋ณด์ฌ์ค ์ ์๋ ๋ฐฉ๋ฒ์ ๊ณ ๋ฏผํด ๋ณธ ๋ค, ๊ฐ ์์๋ง๋ค Elements๋ฅผ ๋ถ๋ฆฌํด ๋ ๋๋งํ ์ ์๊ฒ ํ์์ต๋๋ค.
- ๊ฐ๊ฐ์ ์์์ ๊ด๋ฆฌ ์ปดํฌ๋ํธ๋ฅผ ๋๋๋๋ค. ex) TextEditor
- ๊ฐ๊ฐ์ Editor ์ปดํฌ๋ํธ์์ ํด๋น ํ๋กํผํฐ์ ๋ง๋ action์ dispatch ํฉ๋๋ค.
- ์์ฑ๋ ๊ฐ์ฒด๋ ๊ฐ๊ฐ์ slice์ ํฌํจ๋๊ณ ๋ ๋๋ง ๋ฉ๋๋ค. ex) TextElements
- ์ด์ ๊ฐ๊ฐ์ ๊ฐ์ฒด๋
EditorRenderer์ปดํฌ๋ํธ์์updateNewElementsaction์ ํตํดeditorSlice์allElements๋กzIndex์ ์์๋๋ก ๊ด๋ฆฌํด์ฃผ์์ต๋๋ค.
<ContentBox>
<View>
<TextElements />
<ImageElements />
<GifElements />
<ShapeElements />
</View>
<LayerModal />
<ImageModal />
<GifModal />
<FontModal />
<ColorModal />
<IconModal />
</ContentBox>useEffect(() => {
dispatch(updateNewElements(textElements));
}, [textElements]);updateNewElements: (state, action) => {
const elements = action.payload;
Object.keys(elements).forEach((key) => {
const element = elements[key];
state.allElements[element.zIndex] = element;
});
},- ์ด๋ฏธ์ง ํด์ ๊ฒฝ์ฐ์๋ ํ์์ ์ผ๋ก ๋ ์ด์ด๋ผ๋ ๊ฐ๋ ์ด ๋์ ์ด ๋ฉ๋๋ค.
- ๊ทธ๋ ๋ค๋ฉด layer๋ฅผ ๋ณํ์ํฌ ํ์๊ฐ ์์ ๋๋ ์ด๋ป๊ฒ ์์๋ค์ ๊ด๋ฆฌํ ์ ์์๊น? ๊ฐ ๋๋ฒ์ฌ ํ๋๋ก ๋ ์ฌ๋์ต๋๋ค.
- ์๊ฐํ ๋ฐฉ๋ฒ์ ์ด๊ฑธ ๋จ๋ฐฉํฅ์ผ๋ก ์ํ๋ฅผ ๊ด๋ฆฌํ๋ฉด ๋์ง ์์๊น? ๋ผ๋ ์๊ฐ์ ํ์์ต๋๋ค.
- ๊ฐ Elements => allElements => layerElements
- ๋ค์ ๋ณ๊ฒฝ๋ layerElements๋ฅผ ํตํด ๊ฐ ์์์ ๋ฟ๋ ค์ฃผ๋ ์์ ์ ์งํํ์์ต๋๋ค.
useEffect(() => {
const updatedElements = Object.keys(layerElements)
.sort((a, b) => layerElements[a].zIndex - layerElements[b].zIndex)
.map((key) => layerElements[key]);
const shapes = [];
const texts = [];
const gifs = [];
const images = [];
updatedElements.forEach((element) => {
switch (element.type) {
case SHAPE:
shapes.push(element);
break;
case TEXT:
texts.push(element);
break;
case GIF:
gifs.push(element);
break;
case IMAGE:
images.push(element);
break;
default:
break;
}
});
if (texts.length > 0) {
dispatch(updateAllTexts(texts));
}
}, [layerElements]);์ด๋ ๊ฒ ๋ณ๊ฒฝ๋ ๊ฐ์ฒด๋ค์ ๋ค์ ๋ ๋๋ง ์์ผ์ฃผ๋ ์์ ๊น์ง ์๋ฃํ์์ผ๋, ์ ์ฅ์ด๋ผ๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
์ด๋ฅผ ์ํด ์กฐ์ฌ๋ฅผ ํด๋ณธ ๋ค ๋๊ฐ์ง์ ๋ฐฉ๋ฒ์ ํ์ธํ๊ฒ ๋์์ต๋๋ค.
- ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ณํ ์์ผ ํ๋์ ์ด๋ฏธ์ง๋ก ๋ง๋๋ ๋ฐฉ๋ฒ
- ํ๋์ ์์ญ์ ์บก์ณํ๋ ๋ฐฉ๋ฒ
์ด ์ค์์ 2๋ฒ์ ๋ฐฉ๋ฒ์ ์ ํํ๋ ๊ฒ์ด ์๋ฌ๋ฅผ ํธ๋ค๋งํ๊ธฐ์ ์ฉ์ดํ ๊ฒ์ด๋ผ ์๊ฐํด ์ ํํ์์ต๋๋ค.
์ด์ ๋ค์ ๋ฌธ์ ๋ Image์ ๊ฒฝ์ฐ์๋ ์ฝ๊ฒ ์บก์ณ๋ฅผ ํ๋ฉด ๋์ง๋ง GIF์ ๊ฒฝ์ฐ ์ด๋ป๊ฒ ์ ์ฅํ ์ ์์๊น?์ ๋ํ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
GIF๋ก ๋ณํ ํด์ฃผ๋ ๊ฒ์ PNG๋ก ์์ญ ๋ด์ ๊ฐ์ฅ ๊ธด GIF์ ๊ธธ์ด ๋งํผ ํ๋ ์๋ง๋ค ์บก์ณํด์ค ๋ค ๊ทธ๊ฒ์ GIF๋ก ๋ง๋ค์ด์ฃผ๋ฉด ๋์ง ์์๊น? ๋ผ๋ ์ ๊ทผ์ผ๋ก ์์ํด ๋ณด์์ต๋๋ค.
PNG๋ฅผ GIF๋ก ๋ฐ๊พธ๋ ๊ฒ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ์ง ์๊ณ ์๋ํด ๋ณด์์ผ๋, PNG๋ฅผ GIF๋ก ๋ฐ๊พธ๋ ๊ฒ์ ์๋นํ ๋ฌด๋ฆฌ๊ฐ ์์๊ธฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํ๊ธฐ๋ก ํ๋จํ์์ต๋๋ค.
์ด์๋ ๋ฌธ์ ๋ ์์์ต๋๋ค.
RN์ด ๊ฐ์ง๋ ํ๊ณ์ :RN์ด๊ธฐ ๋๋ฌธ์PNG๋ฅผGIF๋ก ๋ฐ๊พธ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐNODE๊ธฐ๋ฐ์ด๊ธฐ์ ์ฌ์ฉํ ์ ์์ด ์๋ฒ๋ก ๋ณด๋ด์ค ๋ค ๊ทธ๊ฒ๋ค์ ์๋ฒ์์GIF๋ก ๋ง๋ค์ด ๋ค์RN์ผ๋ก ๋ณด๋ด์ฃผ๋ ๋ฐฉ์์ ์ฑํํ์์ต๋๋ค.- ๋ฒ๊ฑฐ๋ก์ด ์ผ๋ จ์ ์์
:
PNG๋ฅผGIF๋ก ๋ฐ๊พธ๋ ์์ ์ ์๋นํ ๋ง์ ์์ ์ด ํ์๋ก ์ฌ๊ฒจ์ง๋๋ค.
PNG๋ฅผ ํ๋ ์๋ง๋ค ์บก์ณํ์ฌ ๋ฐฐ์ด๋ก ๋ง๋ค๊ธฐ- ์บก์ณํ
PNG๋ฅผbase64๋ก ์ธ์ฝ๋ฉ base64๋ก ์ธ์ฝ๋ฉํPNG๋ค์ ๋ฐฐ์ด์ ์๋ฒ๋ก ๋ณด๋ด์ฃผ๊ธฐ- ์๋ฒ์์๋
base64์ ๋์ฝ๋ฉํ์ฌGIF๋ก ๋ณํํ ๋ค ๋ค์base64๋ก ์ธ์ฝ๋ฉ ํ ํRN์ผ๋ก ๋ฐํ RN์์๋base64๋ฅผ ๋ค์ ๋์ฝ๋ฉํ ํGIF๋ก ์ ์ฅํ๊ธฐ.
- ์ฌ๊ธฐ์์
base64๋ ์ด๋ฏธ์ง๋ฑ์ ์ฝ๊ฒ ๋ณํํ ์ ์๋ ์์คํค์ฝ๋์ ๋ฌธ์๋ค๋ก ์ด๋ฃจ์ด์ง 64์ง๋ฒ์ผ๋ก ์ธ์ฝ๋ฉํ ๋ฐฉ์์ ๋๋ค.
const frames = [];
const encodedFrames = [];
const frameCount = Math.round(duration * 60);
const frameInterval = 1000 / (fps / 2);
for (let i = 0; i < frameCount; i++) {
setTimeout(async () => {
const frame = await captureRef(imageRef, {
format: "png",
quality: 1.0,
});
frames.push(frame);
}, i * frameInterval);
}frame์ด ๋ฐ๋ณต๋๋ ์ ๋งํผcapture๋ฅผ ํด์ค ๋ค
await new Promise((resolve) => setTimeout(resolve, frameCount * frameInterval));
for (const frame of frames) {
const base64Data = await FileSystem.readAsStringAsync(frame, {
encoding: FileSystem.EncodingType.Base64,
});
encodedFrames.push(`data:image/png;base64,${base64Data}`);
}frames์capture๊ฐ ๋ ๊ฒ์ ํ๋ก๋ฏธ์ค๋ฅผ ํตํด ์ฒดํฌํ ๋ค ์ด๊ฒ๋ค์base64๋ก ๋ณํํ ๋ค ๋ค์ ๋ฐฐ์ด์ ๋ด์์ฃผ์์ต๋๋ค.
๊ทธ๊ฒ๋ค์ ์๋ฒ์์ ๋ฐ์ ๋ค
const encoder = new GIFEncoder(Math.round(width), Math.round(height));
encoder.start();
encoder.setRepeat(0);
encoder.setDelay(1000 / fps);
encoder.setQuality(5);
for (const frame of encodedFrames) {
const buffer = Buffer.from(frame.split(",")[1], "base64");
const { data, info } = await sharp(buffer) // sharp๋ PNG ์ฌ์ด์ฆ ์กฐ์ ๋ฐ ์์ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์
๋๋ค.
.resize(Math.round(width), Math.round(height), {
fit: "contain",
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.raw()
.ensureAlpha()
.toBuffer({ resolveWithObject: true });
if (info.width !== Math.round(width) || info.height !== Math.round(height)) {
console.error(
`Skipping frame with invalid dimensions: ${info.width}x${info.height}`,
);
continue;
}
encoder.addFrame(data);
}
encoder.finish();
const gifBuffer = encoder.out.getData();
res.set("Content-Type", "image/gif");
res.send({ base64Gif: gifBuffer.toString("base64") });Buffer๋ฅผ ์ด์ฉํด ํ ํ๋ ์์base64๊ฐ ๋ค ๋ค์ด์ค๋ฉด ํ๋ํ๋๋ง๋คsharp๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํดPNG๋ก ๋ค์ ๋ง๋ค์ด ์ค ๋คGIFEncoder๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํ๋ ์์ ํ๋ํ๋ ๋ฃ์ด์ค ๋คGIF๋ก ๋ง๋ค์ด ์ฃผ์์ต๋๋ค.
ํ๋์ ์ฑ๊ณต์ ์ธ GIF๊ฐ ์์ฑ๋์์ต๋๋ค!
์ด๋ป๊ฒ ํ๋ฉด ์ ์ ์นํ์ ์ผ๋ก ๋ง๋ค ์ ์์ ๊น?
๋ชจ๋ฐ์ผ ์ด๋ผ๋ ํน์ฑ์ ์๊ฐ๋ฝ์ผ๋ก ์ ์ดํด ์ปจํธ๋กค ํ๋ ๊ฒ๋ค์ ์ฌ์ฉ์ ๊ฒฝํ์ด ์น๋ณด๋ค ๋ ํฌ๊ฒ ๋ค๊ฐ์ฌ ์ ์๋ ์์๋ผ๊ณ ํ๋จํ์์ต๋๋ค.
์ด๋ฅผ ์ํด ์ด๋ป๊ฒ ํ๋ฉด ์ข ๋ ์ ์ ๋ค์ด ๋๋ผ๊ธฐ์ ํธํ๊ฒ ํ ์ ์์๊น? ๋ผ๋ ๊ณ ๋ฏผ์ด ์๊ฒผ์ต๋๋ค.
Layer์ ์์๋ฅผ ๋ณ๊ฒฝํ ๋ ๋๋๊ทธ ์ค ๋๋์ ํ์ฉํ๋ ๊ฒ์ด ์ฌ์ฉ์ ๊ฒฝํ ๋ฉด์์ ์ข ๋ ์นํ์ ์ผ ๊ฒ์ด๋ผ ํ๋จํ์์ต๋๋ค.- ์ด๋ฅผ ์ํด
PanResponder์ ์ด๋ฒคํธ ์์์ ๋๋๊ทธํ ๋ ์ด์ด๋ฅผ ์ฎ๊ธฐ๋ ํ์๊ฐ ์ผ์ ์์น ๋ฒ์ ์ด์์ ๋์ด๊ฐ ๋ ์๋ฃ์ ์์๊ฐ ๋ณ๊ฒฝ๋๊ฒ ํ์์ต๋๋ค. newIndex๋ฅผ ์์ฑํ ๋ค ๊ตฌ์กฐ ๋ถํด ํ ๋น์ ์ด์ฉํด ๋ฐฐ์ด์ ์์๋ฅผ ๋ณ๊ฒฝํด ์ฃผ์์ต๋๋ค.- ๊ทธ ๋ค
zIndex๋ฅผ ์ด์ฉํด ๊ฐ์ฒด๋ค์ ์ฌ์ ๋ ฌ ํด์ค ๋ค ์ ๋ฐ์ดํธ ์์ผ์ฃผ๋ ์์ ์ ์งํํ์์ต๋๋ค.
onPanResponderRelease: (_, gestureState) => {
const positionY = positionRef.current.y;
const indexDiffByPosition = Math.min(
Math.max(
Math.round(Math.abs(positionY) / (SCREEN_HEIGHT * 0.125 * 0.9)),
0,
),
currentElements.length - 1,
);
let newIndex;
if (indexDiffByPosition !== 0 && positionY > 0) {
newIndex = currentIndex + indexDiffByPosition;
}
if (indexDiffByPosition !== 0 && positionY < 0) {
newIndex = currentIndex - indexDiffByPosition;
}
if (newIndex === undefined || newIndex >= currentElements.length) {
return movePan.setValue({ x: 0, y: 0 });
}
[currentElements[currentIndex], currentElements[newIndex]] = [
currentElements[newIndex],
currentElements[currentIndex],
];
const newElements = currentElements.map((element, index) => {
return { ...element, zIndex: currentElements.length - 1 - index };
});
selectedLayerIndex.current = newIndex;
dispatch(updateAllElements(newElements));
},1 ์ฃผ์ฐจ : ๊ธฐํ ๋ฐ ์ค๊ณ
- ์์ด๋์ด ์์ง
- ๊ธฐ์ ์คํ ์ ์
- Git ์์ ํ๋ก์ฐ ๊ฒฐ์
- Figma๋ฅผ ์ฌ์ฉํ Mockup ์ ์
- MongoDb๋ฅผ ์ด์ฉํ DB Schema ์ค๊ณ
- Notion์ ์ด์ฉํ ์นธ๋ฐ ์์ฑ
2 ~ 3 ์ฃผ์ฐจ : ๊ธฐ๋ฅ ๊ฐ๋ฐ ๋ฐ ๋ฐํ
-
RN ๊ตฌํ
-
๋ฐฑ์๋ ์๋ฒ ๊ตฌํ
-
๋ฆฌ๋๋ฏธ ์์ฑ
React Native๋ฅผ ์ฒ์ ๋ค๋ค๋ณด๋ฉฐ ๋๊ผ๋ ์ ์ "์๊ฐ๋ฝ์ด๋ผ๋ ์ ์ ์ผ๋ก ๋ฒ์ด์ง๋, ์ ๋ง์ ์ผ๋ค์ด ์๊ตฌ๋" ์์ต๋๋ค. ์น๋ง์ ๊ณต๋ถํ๋ค๋ฉด ์ง์คํด๋ณด์ง ๋ชปํ์ ์ ์ค์ณ๋ผ๋ ๋ถ๋ถ์ด ์ฌ๋ฏธ์๋ ์์๋ก์จ ์์ฉํ๋ ๊ฒ ๊ฐ์ต๋๋ค.
์ด์ ๋์์ ๊ธฐ์กด์ ์ ๊ฐ ์ฌ์ฉํ๋ ์ฑ๋ค์ด ๊ทธ๋ฅ ํํฌ๋ก ๋ง๋ค์ด์ง ๊ฒ์ด ์๋๊ณ ๋ํ ์ผ์ ์ฑ๊ธด ๋ถ๋ถ๋ค์ด ์ ๋ง ๋ง๊ตฌ๋๋ผ๋ ๊ฒ์ ๋ค์๊ธ ๋๊ปด๋ณด๊ฒ ๋์์ต๋๋ค. ์ด๋ฒ ๊ฒฝํ์ ํตํด ๊ธฐ์กด์ ์ฌ์ฉํ๋ ๋ชจ๋ฐ์ผ ์๋น์ค๋ค์ ์๊ฐ๋ฝ์ด ๋๋ฆด ๋ ๋ถํฐ ๋ผ์ด์ง ๋ ๊น์ง์ ์ผ๋ค์ ๋ํ ๊ด์ฌ์ ๋ง์ด ๊ฐ์ ธ๋ณด๊ฒ ๋ ๊ณ๊ธฐ๊ฐ ๋์ด ์ ์๊ฒ ์ข์ ์ํฅ์ ์ฃผ์์ต๋๋ค.
์ ๋ ์์ผ๋ก๋ ์ด์ ๊ฐ์ด ํด๊ฒฐํด์ผ ํ ์์๋ค์ด ๊ฐ๋ํ ํ๋ก์ ํธ๋ค๊ณผ ํจ๊ปํ ๊ฒ์ธ๋ฐ ์ด ์ํฉ์ด ๊ฐ๋ฐ์ ์ง์์ ์ผ๋ก ํฅ๋ฏธ์๊ฒ ๋ง๋ค์ด ์ค๋ค ์๊ฐํฉ๋๋ค.
์ด๋ฌํ ์ง์์ ์ธ ํฅ๋ฏธ์ ํ๊ตฌ๋ฅผ ํตํด ์ฌ๋ฌ ํ๋ก์ ํธ๋ค์ ํ์ด ๋๊ฐ๋ณด๊ณ ์ถ๋ค๋ ์๊ฐ์ ๊ฐ์ง๊ฒ ๋์ด ์ข์ ๊ฒฝํ์ด์์ต๋๋ค.




