리펙토링을 진행하게된 계기
방치하고 있던 프로젝트가 있었다.
이 프로젝트를 진행할 때 꽤나 공을 들인 프로젝트였는데 코테준비다, 시간이 없다 등 지금 생각하면 배부른 핑개를 대며 프로젝트를 방치했었다.
하지만, 최근 원티드 프리온보딩을 통한 좋은 코드와 SOLID 원칙에 대한 강의를 듣고 프로젝트의 코드들을 전체적으로 살펴봤다.
지금 보니 아주 난장판이였다.
import { ReactComponent as GamaldaSVG } from 'assets/svg/gamaldalogo.svg';
import naverLogin from 'assets/png/naver_login.png';
import { useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
const Nav_Login = () => {
// process.env.REACT_APP_NAVER_CLIENT_ID 는 네이버에서 생성된 client ID, process.env.REACT_APP_NAVER_LOGIN_CALLBACK_URL 는 네이버에서 설정한 콜백 url이다.
let naver_api_url = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${process.env.REACT_APP_NAVER_CLIENT_ID}&redirect_uri=${encodeURI(process.env.REACT_APP_NAVER_LOGIN_CALLBACK_URL!)}&state=${process.env.REACT_APP_NAVER_STATE}`;
const snowEffetRef = useRef<HTMLDivElement>(null);
// 추후 분리 예정
// 로그인 페이지 배경에 눈을 생성해주는 함수
const getParticles = (particleAreaRef: React.RefObject<HTMLElement>) => {
const particles = particleAreaRef;
const border = ["50%", "0%"];
const colors = ["#FF6B6B", "#FFE66D", "#ffffff"];
if (particles === null) return;
var np = document.documentElement.clientWidth / 40;
for (var i = 0; i < np; i++) {
var w = document.documentElement.clientWidth;
var h = document.documentElement.clientHeight;
var rndw = Math.floor(Math.random() * w) + 1;
var rndh = Math.floor(Math.random() * h) + 1;
var widthpt = Math.floor(Math.random() * 8) + 5;
var opty = Math.floor(Math.random() * 4) + 1;
var anima = Math.floor(Math.random() * 12) + 8;
var bdr = Math.floor(Math.random() * 2);
var color = Math.floor(Math.random() * 3);
var div = document.createElement("div");
div.style.position = "absolute";
div.style.marginLeft = rndw + "px";
div.style.marginTop = rndh + "px";
div.style.width = widthpt + "px";
div.style.height = widthpt + "px";
div.style.opacity = opty + "";
div.style.backgroundColor = colors[color];
div.style.borderRadius = border[bdr];
div.style.animation = "move " + anima + "s ease-in infinite";
particles.current!.appendChild(div);
}
};
useEffect(() => {
getParticles(snowEffetRef);
}, [])
return (
<>
<div className="bg"></div>
<div className="login_page flex_center">
<div className="login_box">
<div className="title_area flex_center">
<p className="login_box_title">로그인하고 시작하기</p>
</div>
<div className="img_area flex_center">
<GamaldaSVG width="220px" height="220px" />
</div>
<div className="login_button_area flex_center">
<Link to={naver_api_url} className="flex_center">
<img className="login_button_img" src={naverLogin} alt="네이버 로그인" />
</Link>
</div>
</div>
</div>
<div className="particles" ref={snowEffetRef} />
</>
)
};
export default Nav_Login;
오오.....
꽉꽉 한 컴포넌트에 여러 기능을 잘 만들어 놨다.....
잘 한게 아니다.
이번 첫 리펙토링에서 내가 중점으로 생각한 것은 1) 부수 효과를 발생시키는 함수 정리하기, 2) 가독성 높이기, 3) SOLID 원칙을 최대한 적용하기 이다.
이 세 가지를 중점으로 리펙토링을 진행하고 나온 결과와 느낀점을 적어 보도록 하겠다.
애니메이션 함수 분리하기
애니메이션 함수의 분리를 가장 먼저 시작했다.
이유는 현재 로그인 컴포넌트의 코드 대부분이 이 애니메이션 함수가 차지하기도 했고, useRef Hook을 사용하는데 문제가 있다고 생각했다.
const Nav_Login = () => {
...
const snowEffetRef = useRef<HTMLDivElement>(null);
const getParticles = (particleAreaRef: React.RefObject<HTMLElement>) => {
const particles = particleAreaRef;
...
if (particles === null) return;
var np = document.documentElement.clientWidth / 40;
for (var i = 0; i < np; i++) {
var w = document.documentElement.clientWidth;
var h = document.documentElement.clientHeight;
var rndw = Math.floor(Math.random() * w) + 1;
var rndh = Math.floor(Math.random() * h) + 1;
var widthpt = Math.floor(Math.random() * 8) + 5;
var opty = Math.floor(Math.random() * 4) + 1;
var anima = Math.floor(Math.random() * 12) + 8;
var bdr = Math.floor(Math.random() * 2);
var color = Math.floor(Math.random() * 3);
var div = document.createElement("div");
div.style.position = "absolute";
div.style.marginLeft = rndw + "px";
div.style.marginTop = rndh + "px";
div.style.width = widthpt + "px";
div.style.height = widthpt + "px";
div.style.opacity = opty + "";
div.style.backgroundColor = colors[color];
div.style.borderRadius = border[bdr];
div.style.animation = "move " + anima + "s ease-in infinite";
particles.current!.appendChild(div);
}
};
useEffect(() => {
getParticles(snowEffetRef);
}, [])
return (
<>
...
<div className="particles" ref={snowEffetRef} />
</>
)
};
export default Nav_Login;
useRef를 useState로 바꿔서 애니메이션 기능 구현하기
위의 코드에서 useRef를 통해 특정 div 태그를 참조해 애니메이션 함수로 생성된 div 태그를 추가해 주었다.
하지만 새로운 React 공식 문서에서 useRef 주의사항을 보면 "Ref.current 속성을 변경할 때 Ref는 구성 요소를 다시 렌더링하지 않습니다." 라는 문구를 확인할 수 있다.
실제로 반응형으로 페이지를 설계했지만, 애니메이션 기능만 반응형으로 동작하지 않았다.
그렇다면 반응형으로 애니메이션을 구현하기 위해서는 변경된 값을 적용해 리렌더링 해주는 작업이 필요할 것이다.
으잉? 이거 useState를 이용하면 어쩌면???
그렇다....
우리가 자주 사용하는 useState는 state값이 바뀌면 리렌더링을 하는 React의 특징을 이용하면 되는 것이다.
/**
* setParticles를 prop으로 받아
* @param setParticles
* @param document
*/
export const generateSnowParticles = (setParticles: React.Dispatch<React.SetStateAction<JSX.Element[]>>, document: Document): void => {
setParticles([...createDivForSnowPosition(document.documentElement.clientWidth, document.documentElement.clientHeight)]);
}
/**
* 눈 애니메이션에서 눈들의 위치 정보를 바탕으로 생성한 태그를 담은 배열을 반환해주는 함수
* @param width
* @param height
* @returns particlesArray: Array(눈 조각 div 태그들)
*/
const createDivForSnowPosition = (width: number, height: number): JSX.Element[] => {
const border = ["50%", "0%"];
const colors = ["#FF6B6B", "#FFE66D", "#ffffff"];
const particlesArray: JSX.Element[] = [];
for (let i = 0; i < width / 40; i++) {
particlesArray.push(
<div
key={i}
style={{
position: "absolute",
marginLeft: Math.floor(Math.random() * width) + 1 + "px",
marginTop: Math.floor(Math.random() * height) + 1 + "px",
width: Math.floor(5) + 5 + "px",
height: Math.floor(5) + 5 + "px",
opacity: Math.floor(Math.random() * 4) + 1 + "",
backgroundColor: colors[Math.floor(Math.random() * 3)],
borderRadius: border[Math.floor(Math.random() * 2)],
animation: `move ${Math.floor(Math.random() * 12) + 8}s ease-in infinite`,
}}
/>
);
}
return particlesArray;
};
...
const Nav_Login = () => {
...
const [particles, setParticles] = useState<Array<JSX.Element>>([]);
useEffect(() => {
generateSnowParticles(setParticles, document);
// 사용자가 브라우저의 크기를 변경시 변경된 공간에 애니메이션을 나타내기 위해 addEventListener의 resize 추가
// 마운트 될 때 이벤트 리스너를 더하고, 언마운트 될 때 제거해준다.
// 참고 링크 : https://db2dev.tistory.com/entry/React-resize-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%8B%A4%EB%A3%A8%EA%B8%B0
window.addEventListener('resize', () => {
generateSnowParticles(setParticles, document);
});
return () => {
// cleanUp
window.removeEventListener('resize', () => {
generateSnowParticles(setParticles, document);
});
};
}, [])
return (
<>
...
<div className="particles">{particles}</div>
</>
)
};
export default Nav_Login;
createDivForSnowPosition 함수를 통해 만들어진 눈 요소(div 태그로 만들어짐)들은 배열로 담겨 반환된다.
generateSnowParticles 함수에서는 createDivForSnowPosition의 반환 값인 배열을 인수로 받은 setParticles 함수를 이용해 state를 변경해주었다.
하지만 위의 코드들 에서도 뭔가 부족하다고 느꼈다....
우선 1) state 변경을 위한 함수가 하나 추가 됬다는 것, 2) useEffect 내부에서 resize 이벤트 리스너가 있다는 것이다.
state 변경을 위한 void 함수는 컴포넌트 useEffect 내에서 set 함수를 이용하는 방향으로 바꿨다.
그리고 resize 이벤트의 경우는 어떻게 할까 고민을 했다.
resize의 경우에는 사용자가 화면의 크기에 변화를 주었을때도 바뀐 크기에 맞는 애니메이션 효과를 주기 위해 적용한 것이다.
그래서 나는 resize 이벤트 리스너를 이용해 바뀐 크기 값을 갖는 객체를 반환하는 Hook을 만들었다.
import { useEffect, useState } from "react";
interface windowSizeCustomType {
windowWidth: number;
windowHeight: number;
}
const useResizedWindowSize = (): windowSizeCustomType => {
const [windowSizeobj, setWindowSizeObj] = useState({
windowWidth: document.documentElement.clientWidth,
windowHeight: document.documentElement.clientHeight,
});
const handleResize = () => {
setWindowSizeObj({
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
});
};
useEffect(() => {
if (typeof window !== 'undefined') {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
} else {
return () => window.removeEventListener("resize", () => {
return null;
});
}
}, []);
return windowSizeobj;
}
export default useResizedWindowSize;
이렇게 만들어진 Hook을 이용하고 set 함수를 useEffect내에서 사용해 아래와 같은 결과 코드를 만들었다.
...
const Nav_Login = () => {
const [particles, setParticles] = useState<Array<JSX.Element>>([]);
const windowSize = useResizedWindowSize();
useEffect(() => {
setParticles([...SnowParticles(windowSize.windowWidth, windowSize.windowHeight)])
}, [windowSize])
return (
<>
...
<div className="particles">{particles}</div>
</>
)
};
export default Nav_Login;
리펙토링을 통해 코드가 간결해진 모습을 볼 수 있다.
또한, useResizeWindowSize Hook과 SnowParticles는 해당 컴포넌트가 아닌 다른 컴포넌트에서 사용할 때에도 쉽게 사용할 수 있는 모습이 되었다.
그리고 작업된 함수들은 오직 한가지 기능을 위해 만들어 졌으며, SnowParicles의 경우는 순수 함수의 모습 갖게 되었다.
결과
작업한 함수와 컴폰너트들이 한가지의 기능과 의도를 갖게 했으며,
SRP 원칙을 적용해 페이지 컴포넌트에서 각각의 함수, 컴포넌트로 분리해 테스트와 오류 수정이 용이해 졌다.
또한, ISP 원칙을 적용하며 불필요한 props를 넘기지 않아 함수와 컴포넌트 사용에 있어 이점을 챙길 수 있었다.
결과적으로 SOLID원칙 중 SRP(단일 책임 원칙)과 ISP(인터페이스 분리 원칙)을 최대한 적용했고, 추후 다른 컴폰너트에 애니메이션을 적용할 경우를 대비한 확장 용이성까지 높일수 있던 작업이였다.
또한, useState와 useRef를 통해 React 함수형 컴포넌트에서 동적으로 상태 관리를 할 수 있지만, Rerendering이 필요한 경우에 useRef가 속성에 변화를 주어도 리렌더링이 되지 않기 때문에 useState를 이용해 구현하는 것이 더 좋다는 것을 알게되었다.
참고자료
'프로젝트 > 가말다 - 마일스톤' 카테고리의 다른 글
OAuth와 소셜로그인(Naver Login API) (1) | 2024.04.22 |
---|---|
오류) 도메인 변경에 따른 이미지 서버 관련 오류 발견 (0) | 2024.03.23 |
이미지 파일 서버에 전송 후 저장, 그리고 이미지 주소 이용 (0) | 2023.08.26 |
Store과 DB를 혼동해 사용하지 말자. (0) | 2023.05.16 |
로그아웃 기능 중 쿠키 전달을 위한 fetch의 credentials(내용증명) 옵션 사용 (0) | 2023.04.19 |