이번 과제는 온라인 커머스에 노출되는 상품의 신규 등록 or 수정하는 영역을 만드는 것이였다.
기업 S의 실제 기획서를 받았고 현업에서 실제 사용한 것을 보이는 pdf 문서를 받았다.
디자이너가 작업한 화면과 함께 각 컴포넌트, 버튼들의 [내용 및 표시], [동작 정책]이 기재되어 있었다.
21가지의 크고 작은 기능과 함께 9개의 컴포넌트로 나뉘어졌고 팀원들과 각각 역할 분배를 했다.
아래의 전체적인 개발 순서는 팀원들과 프로젝트의 우선순위 및 진행 방향의 주관적인 의견을 공유 하기 위해서 작성한 순서이다.
프로젝트의 volume이 크다고 생각되어 선택과 집중이 필요해보였다. 그래서 우선순위를 크게는 다음과 같이 작성했다.
CRA + ESLint & Prettier, Airbnb Style Guide
절대경로
컴포넌트간 구별을 위한 inline style (border) 등만 하기
개발후 pull request 날리기
- 아무리 css 안본다고 해도 그래도 봐줄만 한게 리팩토링보다 우선이라 생각합니다.
- 과연 리팩토링 할 수있을까요 ...ㅎㅎ
( 구현화면 캡쳐 - CSS는 무길님이 도와주셨다! 🙇🏻♂️)
정보고시 컴포넌트는 상품의 부가적인 정보를 기재하는 부분이다.
'항목 추가' 버튼을 누르면 자유롭게 양식을 추가할 수 있고 삭제할 수 있다.
'정보고시 추가' 버튼을 누르면 정보고시 자체가 추가된다.
내가 생각한 이번 프로젝트의 궁극적인 목표는 사용자가 form에 작성한 contents를 json으로 export 하는 것이라 생각해서 다음 예시와 같이 객체를 설계했다.
// 상품 정보 고시 JSON 예시
"productInfo": [
{
"id": 1,
"nameAndWeight": "설날한우맞춤선물세트 베이직 A/0.8kg",
"originAndIngredient": "국내산/등심/채끝, 생차돌",
"grade": "(등심 & 채끝 로 1등급)(생차돌 1+등급 이상)",
"storeMethod": "-2~10도 이하 냉장 보관",
"typesOfFood": "포장육(비살균제품)",
"moreInfo" : {},
},
{
"id": 2,
"nameAndWeight": "설날한우맞춤선물세트 베이직 B/0.8kg",
"originAndIngredient": "국내산/등심/채끝, 생차돌",
"grade": "(등심 & 채끝 로 1등급)(생차돌 1+등급 이상)",
"storeMethod": "-2~10도 이하 냉장 보관",
"typesOfFood": "포장육(비살균제품)",
"moreInfo" : {},
},
],
정보공식의 핵심 기능인 동적인 양식(dynamic form)을 다뤄 본적이 없었는데 Create Dynamic Form Fields in React를 통해서 배울 수 있었다.
const INFO_NOTI_TEMPLATE = {
nameAndWeight: "",
originAndIngredient: "",
grade: "",
storeMethod: "",
typesOfFood: "",
};
const [inputFields, setInputFields] = useState([{ ...INFO_NOTI_TEMPLATE }]);
// onChange 이벤트
const handleChangeInput = (index, event) => {
const values = [...inputFields];
// ⭐️ inputFields중에서 작성 중인 index 번째의 form의 name을 key로 하고 value를 넣어준다
values[index][event.target.name] = event.target.value;
setInputFields(values);
};
추가로 항목 추가해서 작성된 옵션들을 기존의 state에 merge 해준다
const mergeToInputFields = (index, moreValues) => {
moreValues.forEach((obj) => {
const [moreKey, moreValue] = Object.values(obj);
// 추가 항목의 key나 value가 공백이면 merge 무시
if (moreKey === "" || moreValue === "") return;
const values = [...inputFields];
values[index][moreKey] = moreValue;
setInputFields(values);
});
};
이미지 추가시 오른쪽에 이미지 이름과 함께 삭제를 할 수 있는 버튼이 노출된다.
아래는 이미지 추가 버튼을 누른 후에 일어나는 event이다.
const onImgChange = (event) => {
event.preventDefault();
//FileReader API를 사용
const reader = new FileReader();
// 업로드한 파일값을 얻어오기
const file = event.target.files[0];
// 파일 업로드 작업이 끝났을 때 실행
reader.onloadend = () => {
if (type === "multiple") { // 복수 이미지
setImgFiles([
...imgFiles,
{
name: file.name,
},
]);
} else { // 단일 이미지
setImgFiles([
{
name: file.name,
},
]);
}
};
reader.readAsDataURL(file); // 파일 경로
};
필터 태그는 미리 지정되어진 리스트 중에서, 사용자에게 예시로 보여준다.
사용자는 원하는 필터 태그를 검색하거나 예시의 필터태그를 클릭해서 원하는 필터 태그를 지정한다.
검색어 텍스트 일치(’자’ 기준)’가 높은 순으로 리스팅
일치율이 동일한 경우, 서버에서 내려주는 데이터 순으로 노출
검색 결과가 없는 경우 ‘검색 결과 없음' 안내
// 검색 알고리즘
const handleSearchChange = (event) => {
const searchWord = event.target.value;
if (searchWord === "") return;
// indexOf가 -1이면 일치하는 단어가 없다
const result = FILTER_TAG_LIST.filter(
(string) => string.indexOf(searchWord) !== -1
);
setSavedTagList(result);
};
상품코드는 임의로 만들어야 해서 많이 실제로 많이 사용하는 느낌인 영어와 숫자를 결합한 코드를 만들기로 했다. (9자리 * 16진수 => 16^9(6경?) 개의 상품 코드 생성 가능)
const productCode = Math.floor(Math.random() * 10000000000)
.toString(16)
.toUpperCase();
모든 폼에서 작성된 state를 최종적으로 병합하는 작업을 App.js에서 진행했다.
이렇게 많은 데이터를 작성하면서 스스로 이게 과연 최선일까?
라는 의문이 들었었다.
프로젝트의 규모가 커질 수 록 상태관리가 중요하다고 하는데 앞으로 더 개선될 수 있는 인사이트를 얻고 싶다.
function App() {
// default state template 작성
const [data, setData] = useState({
exposureOrSalePeriod: { exposure: "", sell: "" },
basicProductInformation: {
savedTagList: [],
selectedTags: [],
inputFields: {},
}
// .. (생략) 다양한 state 들 ...
etcSetting: { providingThankscard: true },
});
return (
<S.Main>
<S.Header>
<h3>상품 등록</h3>
<button type="button" onClick={() => console.log(data)}>
저장하기
</button>
</S.Header>
// 모든 컴포넌트에 state 전파
<ExposureSellPeriod data={data} setData={setData} />
// .. (생략) 다양한 state 들 ...
<Etc data={data} setData={setData} />
</S.Main>
);
}
각각의 컴포넌트에서 부모에게 state를 전달(동기화) 하는 방법은 useEffect를 사용하였다.
// src/components/ExposureSellPeriod/index.jsx
useEffect(() => {
setData({
...data,
exposureOrSalePeriod: {
...data.exposureOrSalePeriod,
exposure, // state injection
sell,
},
});
}, [exposure, sell]); // trigger states
모든 과제는 기본으로 CRUD(Create, Read, Update, Delete)기능에 추가적인 기능이 덧데어진다.
이번 과제를 하면서 검색
, 동적 폼 다루기
, 이미지 첨부
, 상태 관리
, 중첩 객체 다루기
에 대해서 배울 수 있었다.
리액트를 사용하면서 setState는 '비동기 처리'가 되는지도 모른채 사용했던 자신을 반성하고, useEffect를 다루면서 hooks사용에 좀더 능숙해질 수 있었다.
시간이 부족해서 CSS를 도움 받았고, 리팩토링을 진행하지 못한 것이 아쉽다. 역량을 끌어올려서 다음과제는 완벽하게 해내고 싶다.
추가로 소소하게 시간이 뺏길일이 있었는데 그 경험을 적어서 나중에 같은 실수를 반복하지 않고자 한다.
컴포넌트는 항상 대문자로 시작 (이거 관련해서 에러도 없다 🤬)
onFocus이벤트의 반대는 OnBlur다