선택 과제) 본인이 작성했던 코드 중에서 데이터 / 계산 / 액션을 분리해 보고 느낀 점 블로그에 정리하기
다음과 같은 순서로 코드를 정리해보세요.
- 복잡하다고 생각하는 컴포넌트나 함수에서 데이터 / 계산 / 액션을 표시해 보세요.
- 계산 로직에 해당하는 코드들을 컴포넌트 바깥으로 분리해 보세요.
- 이 과정에서 새롭게 생성되는 함수에는 적절한 이름을 붙이고, 인자로 전달되지 않던 암묵적 의존성은 파라미터로 받도록 함수를 수정해 보세요.
- 개선 전과 후에 어떤 차이가 있었는지 설명을 덧붙여보세요.
1차 과제의 대상을 가장 최근의 개인프로젝트 오늘하날로 정했다.
https://github.com/yeonhub/PP-todays_hanal
GitHub - yeonhub/PP-todays_hanal: [개인 프로젝트] 오늘 하날 SNS
[개인 프로젝트] 오늘 하날 SNS. Contribute to yeonhub/PP-todays_hanal development by creating an account on GitHub.
github.com
1. 데이터/계산/액션 구분
WonderPopup.jsx 컴포넌트의 경우 궁금해요 게시글을 클릭 시 나오는 popup 컴포넌트이다.
답변이 달리지 않은 게시글의 경우 사진과 날씨 정보를 업로드 할 수있는 popup이 나오게 된다.
// WonderPopup.jsx
const wonderBoard = useSelector(state => state.board.wonderBoard)
let currentAnswers
// 게시글에 답변이 있는 경우
if (answers) {
currentAnswers = wonderBoard.find(item => item.wonderBoardId === wonderBoardId).answers
}
let answerShowAuthorAcountId, answerShowAuthorLike, answerShowDate, answerShowTime, answerShowWeather, answerShowYesterday, selectedShowImage, answerShowNickname, answerShowTreeLevel
if (currentAnswers) {
const { selectedImage, answerAuthorAcountId, answerDate, answerTime, answerWeather, answerYesterday, answerAuthorLike } = currentAnswers[0]
answerShowAuthorAcountId = answerAuthorAcountId
.
.
.
return (
.
.
.
<div className="btn">
{
// 게시글에 답변이 있는 경우와 없는 경우
answers ?
<button className='goList' onClick={() => offWonder()}>목록으로</button>
:
<>
<button onClick={() => onUploadAnswer()}>알려줄게요</button>
<button onClick={() => offWonder()}>나만알래요</button>
</>
}
</div>
// boardSlice.jsx
{
answers: [
{
answerAuthorAcountId: 4,
answerAuthorLike: 50,
answerDate: "2023-08-01",
answerTime: "15시 50분",
answerWeather: "BsSun",
answerYesterday: true,
selectedImage: "./images/sky/sky5.jpg"
}
],
wonderBoardId: 3,
date: '2023-08-01',
time: '11시 50분',
dateTime: 20230801115044,
authorAcountId: 2,
loactionCity: '인천광역시',
loactionGu: '연수구',
images: "./images/sky/sky5.jpg"
},
이제 1회때 배운 내용을 기준으로 데이터 / 계산 / 액션으로 나누어 보았다.
1) 데이터 : 이벤트에 대한 사실. 문자열, 객체 등 단순한 값 그 자체.
2) 계산 : 입력으로 얻은 출력. 순수 함수, 수학 함수 라고 부르기도 함.
3) 액션 : 외부 세계와 소통하므로 실행 시점과 횟수에 의존. 부수 효과를 일으킴.
const WonderPopup = ({ currentItem, offWonder, setOnWonderPop }) => {
// 데이터 + 계산 ?
const location = useSelector(state => state.acount.location);
const city = location.nowLocationCity
const gu = location.nowLocationGu
const acount = useSelector(state => state.acount.acount)
const { wonderBoardId, date, time, dateTime, authorAcountId, loactionCity, loactionGu, images, answers } = currentItem
// 계산
const wonderNickname = acount.find((item) => item.acountId === authorAcountId).nickname;
const wonderTreeLevel = acount.find((item) => item.acountId === authorAcountId).treeLevel;
// 파일 업로드 액션
const [selectedImage, setSelectedImage] = useState(null);
const fileInputRef = useRef(null);
const dispatch = useDispatch()
const navigate = useNavigate()
// 위치 정보 액션
const [sameLocation, setSameLocation] = useState(false)
const [alertBg, setAlertBg] = useState(false)
useEffect(() => {
if (gu === loactionGu) {
setSameLocation(true)
} else {
setSameLocation(false)
}
}, [location])
// 사진 업로드 액션
const handleImageChange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
setSelectedImage(reader.result);
};
reader.readAsDataURL(file);
}
};
const handleButtonClick = () => {
fileInputRef.current.click();
};
// 현재 날씨 액션
// yesterday
const [yesterday, setYesterday] = useState(true)
const onYesterday = (tem) => {
const todayTem = tem === 'hot' ? true : false
setYesterday(todayTem)
}
// seekbar
const [authorLike, setAuthorLike] = useState(50);
const handleChange = (e) => {
setAuthorLike(e.target.value);
};
const [weatherIcon, setWeatherIcon] = useState('BsSun')
const onWeather = weather => {
setWeatherIcon(weather)
}
// 게시글 삭제 액션
const ownerCheck = useSelector(state => state.board.isOwner)
const [bg, setBg] = useState(false)
const onDel = () => {
setBg(true)
}
const sureDel = () => {
setBg(false)
setOnWonderPop(false)
dispatch(offWonderDel())
navigate('/wonder')
dispatch(onWonderDel(wonderBoardId))
}
const wonderDel = useSelector(state => state.board.wonderDel)
useEffect(() => {
if (wonderDel) {
setBg(false)
setOnWonderPop(false)
dispatch(offWonderDel())
}
}, [wonderDel])
// 게시글 답변 추가 액션
const onAnswer = useSelector(state => state.board.onAnswer)
useEffect(() => {
if (onAnswer) {
setBg(false)
setOnWonderPop(false)
dispatch(offOnAnswer())
}
}, [onAnswer])
// answer
// 날짜 계산
const answerToday = new Date();
const answerYear = answerToday.getFullYear();
const answerMonth = String(answerToday.getMonth() + 1).padStart(2, '0');
const answerDay = String(answerToday.getDate()).padStart(2, '0');
const answerSeconds = String(answerToday.getSeconds()).padStart(2, '0');
const answerHours = ('0' + answerToday.getHours()).slice(-2);
const answerMinutes = ('0' + answerToday.getMinutes()).slice(-2);
// 답변 날짜 계산
const answerAcount = localStorage.getItem('localCurrentAcount')
const answerAuthorAcountId = JSON.parse(answerAcount).acountId
const answerDate = `${answerYear}-${answerMonth}-${answerDay}`;
const answerTime = `${answerHours}시 ${answerMinutes}분`;
const answerWeather = weatherIcon
const answerYesterday = yesterday
const answerAuthorLike = authorLike
// 답변 추가 액션
const onUploadAnswer = () => {
if (!sameLocation) {
setAlertBg(true)
return
}
if (!selectedImage || loactionCity !== city) {
return
}
dispatch(addAnswer({ selectedImage, answerAuthorAcountId, answerDate, answerTime, answerWeather, answerYesterday, answerAuthorLike, wonderBoardId }))
}
// 게시글 데이터
const wonderBoard = useSelector(state => state.board.wonderBoard)
let currentAnswers
if (answers) {
currentAnswers = wonderBoard.find(item => item.wonderBoardId === wonderBoardId).answers
}
let answerShowAuthorAcountId, answerShowAuthorLike, answerShowDate, answerShowTime, answerShowWeather, answerShowYesterday, selectedShowImage, answerShowNickname, answerShowTreeLevel
if (currentAnswers) {
const { selectedImage, answerAuthorAcountId, answerDate, answerTime, answerWeather, answerYesterday, answerAuthorLike } = currentAnswers[0]
answerShowAuthorAcountId = answerAuthorAcountId
answerShowAuthorLike = answerAuthorLike
answerShowDate = answerDate
answerShowTime = answerTime
answerShowYesterday = answerYesterday
selectedShowImage = selectedImage
answerShowWeather = answerWeather
answerShowNickname = acount.find((item) => item.acountId === answerAuthorAcountId).nickname;
answerShowTreeLevel = acount.find((item) => item.acountId === answerAuthorAcountId).treeLevel;
console.log(currentItem);
}
2. Wonder.jsx
Wonder 컴포넌트의 경우 날씨가 궁금한 지역의 게시글을 보여주게 되는데 기본적으로 현재 위치를 기반으로 같은 구/군의 게시글을 보여준다.
따라서 현재 위치를 Slice에서 받아오게 되는데 geolocation과 KAKAO API를 사용하므로 분리하면 유지보수하기 좋다고 생각했다.
const Wonder = () => {
const location = useSelector(state => state.acount.location);
const city = location.nowLocationCity
const gu = location.nowLocationGu
const area = [
["시/도 선택", "서울특별시", "인천광역시", "대전광역시", "광주광역시",...]
.
.
.
계산함수를 컴포넌트에서 분리한 뒤 어디에 모아두어야 할까?
실제로 실무에선 어떤 방식으로 사용하는진 모르겠지만... GPT와 다른 블로그 글을 참고하여 utils 폴더를 생성했다.
특히 리펙터링을 할 때 실무에서 어떻게 적용시키는지 (변수명, 함수 길이, 디자인 패턴 등...) 많은 공부가 필요할 것 같다.
📦src
┣ 📂assets
┣ 📂components
┣ 📂hooks
┣ 📂pages
┣ 📂store
┣ 📂utils
┃ ┣ 📜acountUtils.js
┃ ┣ 📜dateUtils.js
┃ ┣ 📜locationUtils.js
┃ ┗ 📜weatherUtils.js
┣ 📜App.css
┣ 📜App.jsx
┗ 📜main.jsx
// locationUtils.js
import { useSelector } from 'react-redux';
const getCurrentLocation = () => {
const location = useSelector(state => state.acount.location);
const city = location.nowLocationCity
const gu = location.nowLocationGu
const area = [
["시/도 선택", "서울특별시",
.
.
(생략)
.
.
];
return {
location,
city,
gu,
area
};
};
export default getCurrentLocation
그 후 Wonder에서 impoert를 해주면 위치 데이터를 잘 받아올 수 있다.
// Wonder.jsx
import useLocationHook from "../hooks/nowLocation";
.
.
.
const Wonder = () => {
.
.
.
// const location = useSelector(state => state.acount.location);
// const city = location.nowLocationCity
// const gu = location.nowLocationGu
const { location, city, gu, area } = getCurrentLocation();
3. Nearby.jsx
Nearby 컴포넌트에선 현재 위치를 기반으로 같은 구/군의 게시글을 보여주게 되는데 select-option과 날짜가 같은 게시글만 해당된다.
날짜 + 시간의 경우 다른 컴포넌트에서도 사용이 많이 되므로 따로 분리하여 관리하면 좋을 것 같기에 Date 내장 함수를 꺼내보았다.
const Nearby = () => {
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, '0');
const day = String(currentDate.getDate()).padStart(2, '0');
const formattedDate2 = `${year}-${month}-${day}`;
const formattedDate3 = `${month}월 ${day}일`
.
.
.
// dateUtils.js
const getCurrentTime = () => {
const currentToday = new Date();
const currentYear = currentToday.getFullYear();
const currentMonth = String(currentToday.getMonth() + 1).padStart(2, '0');
const currentDay = String(currentToday.getDate()).padStart(2, '0');
const currentSeconds = String(currentToday.getSeconds()).padStart(2, '0');
const currentHours = ('0' + currentToday.getHours()).slice(-2);
const currentMinutes = ('0' + currentToday.getMinutes()).slice(-2);
return {
currentToday,
currentYear,
currentMonth,
currentDay,
currentSeconds,
currentHours,
currentMinutes,
};
};
export default getCurrentTime
또한 선택한 지역의 현재 날씨와 기온이 상단에 표시되어야 하므로 똑같이 분리해 주었다.
// Nearby.jsx
const Nearby = () => {
.
.
.
const weather = nowWeather.nowWeather
const temperatures = nowWeather.nowTem
->
import useLocationHook from "../hooks/nowLocation";
const { weather, temperatures } = getCurrentWeather();
// weatherUtils.js
import { useSelector } from 'react-redux';
const getCurrentWeather = () => {
const nowWeather = useSelector(state => state.acount.weather);
const weather = nowWeather.nowWeather
const temperatures = nowWeather.nowTem
return {
nowWeather,
weather,
temperatures
};
};
export default getCurrentWeather
안 된 다
지역에 상관없이 항상 현재 위치의 날씨와 온도가 표시된다.
하지만 이 문제는 방금 작업과 무관하게 option 선택 시 날씨를 받아오는 API 훅에 전달되지 않아 발생하는 문제인 것 같다. 아마도
나중에 수정해 봐야겠다.
4. WonderUpload.jsx
WonderUpload.jsx 컴포넌트에선 궁금해요 게시글에 답변을 추가할 수 있다.
답변을 추가하기 위해선 두 가지 조건이 있는데
1) 로그인 상태일 것
2) 궁금해요 게시글 위치와 현재 위치가 일치할 것
그리고 답변자의 닉네임과 아이콘이 필요하므로 계정 정보가 필요하다.
위 방식과 마찬가지로 계정 데이터를 가져오는 부분을 분리해 보았다.
const WonderUpload = ({ offWonder, setOnWonderUpload, selectedGugun, selectedSido }) => {
const acount = useSelector(state => state.acount.acount)
const localCurrentAcount = JSON.parse(localStorage.getItem('localCurrentAcount'));
const { nickname, treeType, treeLevel, acountId } = localCurrentAcount
const wonderNickname = acount.find((item) => item.acountId === acountId).nickname;
const wonderTreeLevel = acount.find((item) => item.acountId === acountId).treeLevel;
.
.
.
->
import getCurrentAcount from '../utils/acountUtils'
const { acount, localCurrentAcount, wonderNickname, wonderTreeLevel, acountId } = getCurrentAcount();
5. 결과
Wonder -> locationUtils + dateUtils
Nearby -> dateUtils + weatherUtils
WonderUpload -> acountUtils
날씨, 위치, 계정, 날짜에 대한 함수들을 재사용 가능하게 따로 분리하였다.
6. 후기
지금까지 나름 크고 작은 프로젝트를 진행하면서 추상화, 재사용성, 가독성, 유지보수, 리펙터링 등 전~혀 고민하지 않고 구현했던 것 같다.
사실 제대로 배우거나 공부한 적이 없기에 그때그때 혼자만 애자일하게 추가하고 삭제하고 수정하고 하기 바빴기에 그리고 프로젝트 완성만 하면 서버에 올리고 배포할 것이 아니기 때문에 유지보수면에서 크게 생각하지 않았던 것 같다.
하지만 실제로 수익을 전제로 하는 회사의 서비스의 경우 수많은 변수가 발생하고 수정을 해야 하므로 상당히 중요한 부분이라 생각된다. 그 모든 과정들이 회사의 이미지와 사용자의 만족도로 이어져 성공과 실패에 영향을 주기 때문이기에...
오늘 해본 작업들은 어떻게 보면 대단해 보이거나 아무것도 아닐 수 있지만 그동안 나만 쓰는 프로젝트를 만들었던 나에겐 의미 있는 첫걸음이라 생각한다. 사실 챌린지의 목적에 맞게 제대로 한 건지 모르고 개선 가능한 부분이 수두룩할 수도 있겠지만 더 많은 공부가 필요할 것 같다.
📝 오늘의 3줄 요약
1. 신기하게 코드를 짜면 신기하게 고쳐야 한다.
2. 메모리, 용량, 유지보수에 도움이 될 수 있는 프로그래밍을 하도록 아직 공부할 게 많다.
3. 돌고 돌아 처음부터 잘 만들자.
“빨리 가는 유일한 방법은 제대로 가는 것이다.” X2
- 로버트 C. 마틴