프로젝트가 마무리되어 기록하기 위한 정리 글입니다.
(프로젝트 기간: 7일)
# 프로젝트 배경
(1) 개인
웹 Front-End단에서 리액트 공부를 하고 싶었고, 특히 hooks와 styled-componets를 사용해 보고 싶었던 터라 이를 이용한 간단한 웹사이트를 구현해 봐야겠다고 생각했다.
(2) 팀
나의 경우, 요새 대세로 떠오른 FonFon, forest-mt 같은 심리테스트 웹사이트를 만들고 싶었지만,
팀 회의 결과 이미 나와있는 아이템을 따라 하기보단 새로운 콘텐츠를 만드는 것이 나을 것 같다는 결론을 내렸다.
그에 따라 다른 아이템을 고민하던 중 대학생들을 위한 점심 추천 서비스를 만들자는 아이디어가 나왔고,
최종적으로 현재 재학 중인 상명대의 마스코트 고양이인 온순이를 판사 컨셉으로 하여 학교 근처의 음식점을 판결해 주는 서비스를 개발하기로 결정하였다.
판결 로직은 항상 뭐 먹을지 고민하는 친구들 혹은 동기들끼리 의견이 갈리는 경우에 사용할 수 있도록 랜덤으로 하기로 하였고, 결과가 마음에 안 들면 재심을 요청하여 항소, 상고에 걸쳐 총 2번 더 판결 받을 수 있게 구현하기로 하였다.
# 개발 과정
우선, 이번 프로젝트는 React 공부에 중점을 뒀기 때문에 Back-End 신경을 쓰지 않고자 GitHub Pages를 이용해 구축하였고, 초기에 타입스크립트 적용을 했었지만 프로젝트의 규모가 워낙 작아서 빼는 것이 나을 거라 판단되어 사용하지 않았다.
또한, 점심 먹기 전 서비스를 이용하는 특성상 모바일 비중이 압도적일 것으로 예상되어 데스크톱 환경은 지원하지 않기로 하였고 모바일 환경을 중점으로 개발을 시작하였다.
- 데스크톱 화면 (데스크톱 환경에서는 시작 버튼을 비활성화함.)
디렉토리 구조는 크게 세 갈래로, 재사용 할 수 있는 컴포넌트들을 분리해 놓은 폴더와 음식 이미지들을 모아놓은 폴더, 화면을 구성할 컴포넌트들을 만들어놓은 폴더로 구성했다.
- 디렉토리 구조
화면 컴포넌트는 로딩화면을 보여줄 LoadingScreen, 판결을 위한 시작 버튼이 있는 FirstScreen, 판결을 내려주는 SecondScreen으로 구성하였고 SecondScreen에서 항소, 상고 기능을 모두 구현하였다.
LoadingScreen의 경우 npm에서 react-loading 모듈을 설치해 작성했고,
FirstScreen의 경우 외곽 태그 안에 데스크톱 환경에서 보여줄 JSX와 모바일 환경에서 보여줄 JSX를 두 개다 구현한 뒤 CSS의 미디어 쿼리를 이용해 분기 처리하였다.
SecondScreen의 경우 제일 탈이 많았던 부분이었는데,
구현해야 할 기능이 크게 재심(항소, 상고), 카카오톡 공유, 결과 캡처의 3가지였다.
우선, 재심 기능은 hooks로 구현했는데 객체지향 방식의 Class 컴포넌트에 익숙해져 있던 터라 함수형 프로그래밍을 지향하는 hooks 개념을 이해하는 것이 처음에 조금 힘들었다.
카카오톡 공유 기능의 경우 카카오 링크 API를 사용하여 쉽게 구현할 수 있었지만,
결과 캡처 기능이 문제였는데 html2canvas 모듈로 순조롭게 구현하는가 싶었지만, 카톡 내 인앱 브라우저에서 작동을 안 하는 버그를 발견하였고, 해결하기 위해 남은 프로젝트 기간 동안 열심히 구글링을 했지만 끝내 해결하지 못했다.
그 결과, 버그가 있는 것을 감수하면서까지 캡처 기능을 구현해야 할 필요가 없다는 판단(스마트폰 자체 캡처 기능이 있기 때문)을 내려 deprecated하게 되었다.
찾아본 결과 많은 개발자분들이 카톡 내에서 외부 브라우저 사용을 하기 위해 문의를 해보았지만 받아들여지지 않고 있다고 한다.
- SecondScreen 컴포넌트 hooks 부분
let imgLoading = 0;
// 이미지 preLoading state
const [loading, setLoading] = useState(false);
// 항소 횟수 state
const [count, setCount] = useState(0);
// 랜덤 인덱스 값 state
let [index, setIndex] = useState(foodRandomIndex());
// 사진 state (고양이1 -> 고양이2 -> 음식점)
const [foodPicture, setFoodPicture] = useState("");
// 음식이름 state
const [foodName, setFoodName] = useState("");
// 판결 대사 첫 마디 state
const [description1, setDescription1] = useState("");
// 판결 대사 두 번째 마디 state
const [description2, setDescription2] = useState("");
// 메인 메뉴 state
const [mainMenu, setMainMenu] = useState("");
// 애니메이션 효과(fade_in, typing) state
const [animationEffect, setAnimationEffect] = useState(true);
// 버튼 명 state (항소 -> 상고)
const [buttonName, setButtonName] = useState("항소");
// 버튼 숨김 조절 state
const [buttonHidden, setButtonHidden] = useState("");
/* 카톡 인엡 브라우저에서 실행 안되는 문제로 deprecated */
// 결과 캡처 버튼 이벤트 함수
const captureButtonClick = () => {
html2canvas(document.querySelector("#root"), {
allowTaint: true,
useCORS: true,
}).then((canvas) => {
let download = document.getElementById("captureDownload");
download.href = canvas
.toDataURL("image/png")
.replace("image/png", "image/octet-stream");
download.download = "점심 판결 결과.png";
download.click();
});
};
// 카톡 공유 버튼 이벤트 함수
const kakaoButtonClick = () => {
document.getElementById("kakao-link-btn").click();
};
// 다시 하기 버튼 이벤트 함수
const againButtonClick = () => {
window.location.reload();
};
// 항소(상고) 하기 버튼 이벤트 함수
const appealButtonClick = () => {
setCount(count + 1);
restaurant.splice(index, 1);
setIndex(foodRandomIndex());
imgLoading = 0;
};
// Hooks로 LifeCycle 구현
useEffect(
() => {
setLoading(false);
// 이미지 preLoading
let preLoad1 = new Image();
preLoad1.src = foodCat[count].first;
preLoad1.onload = function () {
imgLoading++;
};
let preLoad2 = new Image();
preLoad2.src = foodCat[count].second;
preLoad2.onload = function () {
imgLoading++;
};
let preLoad3 = new Image();
preLoad3.src = restaurant[index].logo;
preLoad3.onload = function () {
imgLoading++;
};
setInterval(() => {
if (imgLoading === 3) {
setLoading(true);
}
}, 100);
setFoodPicture(foodCat[count].first);
setDescription1(descriptionComment[count].comment1);
setDescription2("");
setFoodName("");
setMainMenu("");
setAnimationEffect(true);
setButtonHidden("none");
if (count === 1) {
setButtonName("상고");
}
setTimeout(() => {
setFoodPicture(foodCat[count].second);
setDescription2(descriptionComment[count].comment2);
}, 6000);
setTimeout(() => {
setFoodPicture(restaurant[index].logo);
setFoodName(restaurant[index].name);
setMainMenu(restaurant[index].mainMenu);
}, 8500);
setTimeout(() => {
setButtonHidden("");
setAnimationEffect(false);
}, 9000);
},
[count, index] // LifeCycle 의존 값(count, index) 명시
);
또한, SecondScreen은 유일하게 외부 CSS 파일이 없는데, JSX 내부의 모든 태그를 styled-componets화하여 구현하였다.
원래 인라인 스타일링을 선호하지 않는 편이지만 막상 사용을 해보니 왜 styled-componets가 대세로 떠오르는지 알 것 같았다.
CSS를 컴포넌트화함으로써 보다 더 직관적인 코드 작성이 가능했고 특히, CSS에 props 메커니즘을 사용할 수 있다는 것에 깜짝 놀라게 되었는데 덕분에 골머리를 앓고 있던 Virtual DOM에서의 animation 적용을 해결할 수 있었다.
- econdScreen 컴포넌트 styled-componets 부분
/* styled-components 사용 */
const fade_inAnimation = keyframes`
0% {
opacity: 0;
}
100% {
opacity: 1;
}
`;
const SecondScreenLayout = styled.div`
position: fixed;
width: 100vw;
height: 100%;
${(props) => {
if (props.fade_inEffect) {
return css`
animation: ${fade_inAnimation} 5s;
`;
}
}}
`;
const StyledMainLayout = styled(MainLayout)`
height: 87%;
`;
const ButtonSection = styled.div`
height: 13%;
padding-left: 5%;
padding-right: 5%;
display: flex;
align-items: center;
justify-content: space-between;
`;
const StyledButton = styled(Button)`
display:${(props) => props.hidden || ""}
height: 7vh;
border-radius: 1rem;
font-size: 3vmax;
`;
# 프로젝트 결과
솔로몬 온순이의 점심 판결 사이트 ☞ 솔로몬 온순이의 점심 판결
(말씀드렸다 싶이 데스크톱 환경은 지원하지 않습니다!)
- React 프로젝트 확인(React Developer Tools)
프로젝트 완료 후, 에브리타임에 서비스 홍보 글을 작성하였고,
그 결과, 실시간 인기 글에 올라갈 정도로 뜨거운 반응이어서 뿌듯했고 유종의 미를 거둘 수 있었다.
- 실시간 인기 글 등극!
- 서비스 홍보 글 본문
- 댓글 반응 중 일부
마지막으로,
리액트가 처음인데도 잘 따라와 준 지우,
음식 그림들 너무 잘 그려주신 지수님,
온순이 그림들 정말 귀엽게 그려주신 수진님
모두 고생하셨고 감사합니다!
'회고 > Project' 카테고리의 다른 글
[Project] 본방사수 (0) | 2020.04.06 |
---|