[유데미x스나이퍼팩토리] 10주 완성 프로젝트 캠프
🍊 프로젝트 1주차
1. 프로젝트 개요
목적
상세페이지 레이아웃 자동화를 구현하고, 홈페이지 관리자 또는 관계자들이 쉽고 빠르게 편집할 수 있게 하기 위해 본 프로젝트를 진행하게 되었다. 페이지를 간편하게 편집하고 만들 수 있도록 하는 시스템이 없어 개발자가 일일이 수정을 해야하기 때문에 이러한 시스템이 필요하다.
상세 서비스
- 로그인
- 개인 정보 설정을 위한 로그인 기능 제공
- 메뉴 관리
- 홈페이지 상단 메뉴 구성들을 편집할 수 있는 기능 제공
- 페이지 관리
- 페이지 링크, 이름, 디자인 수정에 관한 관리 기능 제공
- 상세 페이지 디자인 편집
- 이미지
- 구분선
- 목록
- 텍스트
- 표
- 레이아웃
기능 정의
구분 | 필요 기능 | 래퍼런스/상세설명 |
---|---|---|
로그인 | 관리자 로그인 | 이메일, 비밀번호로 구성된 로그인 localStorage를 활용한 로그인 기능 구현 |
메뉴 관리 | 메뉴 항목 추가 메뉴 설정 메뉴 삭제 |
메뉴 항목 추가 클릭 시 새로운 메뉴를 추가 메인 메뉴 드롭다운 시 하위 메뉴 확인 가능 메뉴마다 제목, 링크를 설정 할 수 있고 삭제 기능이 있음 |
페이지 관리 | 페이지 상세보기 페이지 복제 페이지 디자인 |
페이지 상세보기: 페이지 명, 경로 확인 페이지 복제: 페이지를 복사하여 페이지 명, 상세주소를 입력하여 사용 가능 페이지 디자인: 편집 페이지로 이동 |
상세 편집 페이지 | 블록 추가 디자인 추가 디자인 리스트 모달 퀵 메뉴 |
필요한 기능을 추가하기 위한 컨테이너 블록을 추가하는 기능 구현 블록을 이룰 컨텐츠의 레이아웃 디자인 추가 기능 구현 구현된 레이아웃 별 상세 편집 기능 구현 (i.e. 이미지 레이아웃 ⇒ 이미지 업로드 기능) 블록에 대한 편집, 이동, 삭제 등 블록별 퀵메뉴 구현 |
구조도
기대 효과
- 쉽고 빠르게 레이아웃을 변경 할 수 있다.
- 개발자가 아닌 일반 관리자 or 관계자도 사용 할 수 있다.
- 페이지 관리를 통해 페이지를 쉽게 관리하고, 다양한 디자인의 페이지를 간편하게 만들 수 있다.
- 메뉴 관리를 통해 홈페이지 메뉴들의 구조를 간편하게 설정 할 수 있다.
폴더 설계
2. 기술 스택
CRA (Create React App):
CRA를 선택한 이유는 프로젝트의 초기 설정과 구성을 효율적으로 처리하기 위해서입니다. CRA를 사용하면 빠른 시작과 개발 환경 설정이 가능하며, 기본적인 프로젝트 구조와 설정을 자동으로 생성해줍니다. 이를 통해 프로젝트 초기에 시간을 절약하고 개발에 집중할 수 있습니다.
React-Router:
React-Router를 도입한 이유는 단일 페이지 애플리케이션을 개발하면서 다양한 라우팅 및 네비게이션 요구사항을 처리하기 위해서입니다. React-Router를 사용하면 사용자 경험을 향상시키고 다양한 화면 간 전환을 원활하게 구현할 수 있습니다.
Styled-Components:
Styled-Components를 선택한 이유는 컴포넌트 기반의 스타일링을 채택하여 컴포넌트의 스타일을 관리하기 용이하게 하기 위해서입니다. Styled-Components를 사용하면 CSS-in-JS 방식으로 스타일을 작성하며, 컴포넌트와 스타일의 결합으로 코드 유지보수를 용이하게 할 수 있습니다.
EsLint & Prettier:
EsLint와 Prettier를 도입한 이유는 코드 품질과 일관성을 유지하기 위해서입니다. EsLint는 코드 스타일을 검사하고 일관성 있는 코딩 가이드를 준수할 수 있게 도와주며, Prettier는 코드 포맷팅을 자동화하여 코드의 가독성을 향상시킵니다.
Redux-Toolkit:
Redux-Toolkit을 선택한 이유는 상태 관리를 효과적으로 처리하고 복잡한 애플리케이션 상태를 관리하기 위해서입니다. Redux-Toolkit은 간결하고 직관적인 코드 작성을 지원하며, Redux의 기능을 향상시켜 개발자들이 더욱 효율적으로 상태 관리를 할 수 있게 도와줍니다.
3. Communication
ZEP (Zoom, Email, Phone):
ZEP를 선택한 이유는 실시간 회의와 비대면 소통을 위해서입니다. Zoom을 통한 화상 회의는 팀원들과 실시간으로 의견 교환 및 업무 논의가 가능하며, Email과 Phone을 활용하여 긴급한 문제나 중요한 정보를 공유하는 데 활용될 수 있습니다.
Notion:
Notion을 선택한 이유는 프로젝트 관리와 협업을 위한 중앙화된 플랫폼을 제공하기 위해서입니다. Notion을 사용하여 프로젝트 일지, 작업 목록, 문서, 일정 등을 효과적으로 관리하고 팀원 간 정보 공유를 원활하게 할 수 있습니다.
Github:
Github를 선택한 이유는 버전 관리와 협업을 위한 플랫폼으로 활용하기 위해서입니다. 코드의 변경 이력을 추적하고 리뷰어들과 코드 리뷰를 진행할 수 있으며, 이슈 트래킹을 통해 프로젝트의 문제와 개선 사항을 관리할 수 있습니다.
Figma:
Figma를 선택한 이유는 디자인 및 UI/UX 작업을 위한 협업 도구로 활용하기 위해서입니다. 팀 내 디자이너와 개발자가 함께 작업하고 디자인 컴포넌트를 공유하여 일관된 디자인을 유지할 수 있으며, 프로토타이핑을 통해 디자인 아이디어를 시각화하고 공유할 수 있습니다.
4. 메인 기능 만들기[TEST]
Model.jsx
import PropTypes from 'prop-types';
import { useState, useEffect } from 'react';
import { styled } from 'styled-components';
const Modal = ({ showModal, setShowModal, addImage, selectedImages }) => {
const [selected, setSelected] = useState(selectedImages || []);
useEffect(() => {
setSelected(selectedImages);
}, [selectedImages]);
// 모달이 열릴 때마다 선택된 이미지 상태 초기화
useEffect(() => {
if (showModal) {
setSelected([]);
}
}, [showModal]);
// 모달 외부를 클릭하면 모달이 닫힘
const handleCloseModal = () => {
setShowModal(false);
};
const handleModalContentClick = (e) => {
e.stopPropagation();
};
const handleToggleImage = (image) => {
setSelected((prevImages) => {
if (prevImages.includes(image)) {
return prevImages.filter((img) => img !== image);
} else {
return [...prevImages, image];
}
});
};
// Save 버튼 클릭 시 선택된 이미지들을 배열에 추가하는 함수
const handleSaveButtonClick = () => {
if (selected.length > 0) {
selected.forEach((image) => {
addImage(image);
});
handleCloseModal();
}
};
return (
showModal && (
<Modal_Overlay onClick={handleCloseModal}>
<Modal_Content onClick={handleModalContentClick}>
<CloseButtonContainer>
<p>블록 디자인 추가</p>
<CloseButton onClick={handleCloseModal}>X</CloseButton>
</CloseButtonContainer>
<ContentContainer>
<ImageBox>
<ImageSplit selected={selected.includes('/images/content1.png')}>
<ExampleImage
src="/images/content1.png"
alt="예시 이미지"
onClick={() => handleToggleImage('/images/content1.png')}
/>
</ImageSplit>
<ImageSplit
selected={
selected.includes('/images/content2.png') &&
selected.includes('/images/content2_2.png')
}
onClick={() => {
handleToggleImage('/images/content2.png');
handleToggleImage('/images/content2_2.png');
}}
>
<ExampleImage src="/images/content2.png" alt="예시 이미지" />
<ExampleImage src="/images/content2_2.png" alt="예시 이미지" />
</ImageSplit>
</ImageBox>
<SaveButton onClick={handleSaveButtonClick}>Save</SaveButton>
</ContentContainer>
</Modal_Content>
</Modal_Overlay>
)
);
};
// npm install prop-types
// 타입 체크, prop 유효성 검사
Modal.propTypes = {
showModal: PropTypes.bool.isRequired,
setShowModal: PropTypes.func.isRequired, // Function type 필수
addImage: PropTypes.func.isRequired,
selectedImages: PropTypes.array.isRequired,
};
export default Modal;
const Modal_Overlay = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* 투명한 배경 */
display: flex;
justify-content: center;
align-items: center;
`;
const Modal_Content = styled.div`
background-color: #fff;
border-radius: 8px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
`;
const CloseButtonContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 8px 8px 0 0;
padding: 10px;
background-color: ${(props) => props.theme.colors.orange};
`;
const CloseButton = styled.button`
background: none;
border: none;
font-size: 16px;
cursor: pointer;
padding: 0;
`;
const ContentContainer = styled.div`
background-color: ${(props) => props.theme.colors.gray.lighter};
padding: 10px;
`;
const ImageBox = styled.div`
display: flex;
`;
const ImageSplit = styled.div`
background-color: #fff;
border-radius: 8px;
padding: 10px;
margin: 10px;
border: 2px solid ${(props) => (props.selected ? 'orange' : '#000')};
`;
const ExampleImage = styled.img`
max-height: 200px;
object-fit: cover;
padding: 10px;
`;
const SaveButton = styled.button`
background-color: ${(props) => props.theme.colors.orange};
color: #fff;
padding: 10px;
border: none;
border-radius: 8px;
cursor: pointer;
margin-top: 10px;
`;
Editor.jsx
import { useEffect, useRef, useState } from 'react';
import { styled } from 'styled-components';
import Modal from './Modal';
const Editor = () => {
// 이미지 상태 관리
const [images, setImages] = useState(() => {
const storedImages = localStorage.getItem('editor_images');
return storedImages ? JSON.parse(storedImages) : [];
});
useEffect(() => {
localStorage.setItem('editor_images', JSON.stringify(images));
}, [images]);
// 모달 상태 관리
const [showModal, setShowModal] = useState(false);
// 모달 열기 함수
const handleOpenModal = () => {
setShowModal(true);
};
// 이미지 추가 함수
const addImage = (imageURL, width, height) => {
const newImage = { url: imageURL, width, height };
setImages((prevImages) => [...prevImages, newImage]);
};
// 이미지 삭제 함수
const handleRemoveImage = (image) => {
setImages((prevImages) => prevImages.filter((img) => img !== image));
};
// 이미지 클릭 시 선택된 이미지 상태 관리
const [selectedImage, setSelectedImage] = useState(null);
const [selectedImageSize, setSelectedImageSize] = useState({ width: 0, height: 0 });
// 이미지 클릭 시 input 엘리먼트를 클릭하도록 처리
const imageInputRef = useRef(null);
const handleImageClick = (selectedImage) => {
imageInputRef.current.click();
// 이미지를 클릭하면 해당 이미지를 setSelectedImage 상태에 저장
setSelectedImage(selectedImage);
// 기존 이미지의 넓이와 높이 정보 가져오기
const imgElement = document.createElement('img');
imgElement.src = selectedImage.url;
imgElement.onload = () => {
setSelectedImageSize({ width: imgElement.width, height: imgElement.height });
};
};
// 이미지 파일 선택 시
const handleImageFileChange = (e) => {
const file = e.target.files[0];
if (file) {
const imageURL = URL.createObjectURL(file);
// 선택한 이미지를 새로운 이미지로 대체하여 갱신
setImages((prevImages) =>
prevImages.map((image) =>
image === selectedImage ? { url: imageURL, width: selectedImageSize.width, height: selectedImageSize.height } : image
)
);
imageInputRef.current.value = null;
}
};
return (
<EditorContainer>
<h1>Editor</h1>
<AddImageButton onClick={handleOpenModal}>블록을 선택해주세요</AddImageButton>
<ImageContainer>
{images.map((image, index) => (
<div key={index}>
<Image
src={image.url}
alt={`Image ${index + 1}`}
onClick={() => handleImageClick(image)}
width={image.width}
height={image.height}
/>
<DeleteButton onClick={() => handleRemoveImage(image)}>Delete</DeleteButton>
</div>
))}
<input
type="file"
accept="image/*"
style=
ref={imageInputRef}
onChange={handleImageFileChange}
/>
</ImageContainer>
<Modal
showModal={showModal}
setShowModal={setShowModal}
addImage={addImage}
selectedImages={images}
/>
</EditorContainer>
);
};
export default Editor;
const EditorContainer = styled.div`
padding: 20px;
`;
const ImageContainer = styled.div`
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 20px;
`;
const Image = styled.img`
width: ${(props) => props.width}px;
height: ${(props) => props.height}px;
object-fit: cover;
border-radius: 8px;
cursor: pointer;
`;
const AddImageButton = styled.button`
margin-top: 20px;
padding: 10px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
`;
const DeleteButton = styled.button`
background-color: #ff5a5a;
color: #fff;
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
`;
5. 결과
블록을 추가해주세요 Click
블록 선택 후 Save
이미지 Click File upload
Delete
6. 회고
이번주는 기업 프로젝트를 시작하는 첫주였다. 프로젝트 시작 첫 날에는 헷갈리는 부분도 많았고 어디서부터 어떻게 시작해야 할지 고민을 많이 했다.
팀원들끼리 만나서 프로젝트에 대한 생각과 의견들에 대해서 공유하는 시간을 가졌고, 피그마를 보면서 어떤 기능을 해야 할지 뜯어보며 궁금한 점들에 대해선 질문 리스트를 작성했다.
화, 수, 목, 금 오전 미팅 시간에 멘토님과 ZEP에서 만나 궁금한 점이나 프로젝트 방향에 대해서 말하는 시간을 가졌다. 그리고 노션에 있는 문서 및 회의록 등을 쓰면서 하나 둘 정리를 하며 내 머리속도 정리가 되는 기분이였다.
팀원들과 회의를 진행하면서 이 프로젝트의 핵심과 요지를 잘 파악하는게 제일 중요하지 않을까 하는 생각이 든다.
미팅 시간에 과정이 중요하다는 것이라고 생각한다라고 멘토님께서 말씀하셔서 팀원들과 서로 의견을 많이 내고 소통을 중요 시 하며 진행하는게 좋을거 같다.
노션에 문서 작업을 하면서 프로젝트에 대해 설명하는 부분이 있었는데 그 부분을 작성하면서 이 프로젝트를 하는 이유와 그걸 해결하기 위해 어떠한 시스템을 만들 것인가에 대해 어느정도 방향이 잡힌 듯한 느낌이다.
일단 우리 프로젝트는 웅진씽크빅 홈페이지에서 상세 페이지를 수정 또는 새로 만들 경우 개발자가 계속해서 직접 작업을 해야하는 번거로움이 있는데 이 부분을 개선하기 위해 개발자가 아닌 홈페이지의 관리자 또는 관계자도 쉽고 빠르게 레이아웃 편집을 할 수 있도록 하는 시스템을 만드는 것이 우리의 목표!!
대략적인 기획을 마치고 파트 분배를 세 가지로 나누었는데 나는 메뉴 설정 부분을 맡았다. 하지만 메인 기능이 우선순위가 되었기 때문에 남은 시간에 짬짬히 짜보기로 하자😊
코드의 다형성에 대해서도 생각하는 시간이 있었다. 어떻게 하면 더 좋은 코드로 짤 수 있을지 어떤 학습을 해야 할지 고민을 했지만.. 막상 코딩을 시작하니 신경을 쓸 겨를이 별로 없었던거 같다.
그래서 일단 코드를 짜고나서 수정하는 작업을 고쳐야 할 것 같다. 코딩을 시작하기 전 처음에는 백엔드를 해야할지 말아야 할지 많이 고민을 했는데 막상 해보니까 없어도 되지않나? 하는 생각이 강해졌다.
프론트만으로도 구현이 되고있기 때문이다. 만약에 하다가 진짜 필요하겠는데? 라고 생각이 들었다면 나는 바로 백엔드와 DB연결을 해버렸을 것 같다.ㅋㅋ
생각 할 것도 많고 성공적으로 이 프로젝트를 마칠 수 있을까 걱정도 들지만 이 기회를 발판으로 경험도 쌓고 성장 할 것이라 생각이 들고, 완성도 잘 했으면 좋겠다. 두 마리 토끼 잡기 화이팅👍
본 후기는 유데미-스나이퍼팩토리 10주 완성 프로젝트캠프 학습 일지 후기로 작성 되었습니다.
#프로젝트캠프 #프로젝트캠프후기 #유데미 #스나이퍼팩토리 #웅진씽크빅 #인사이드아웃 #IT개발캠프 #개발자부트캠프 #리액트 #react #부트캠프 #리액트캠프
Leave a comment