1주차(목) - 프리 온보딩 코스 프론트엔드 - 기업 과제 회고

minbr0ther·2022년 2월 1일
0

pre-onboarding-fe

목록 보기
6/15
post-thumbnail

이번 과제는 온라인 커머스에 노출되는 상품의 신규 등록 or 수정하는 영역을 만드는 것이였다.

기업 S의 실제 기획서를 받았고 현업에서 실제 사용한 것을 보이는 pdf 문서를 받았다.

디자이너가 작업한 화면과 함께 각 컴포넌트, 버튼들의 [내용 및 표시], [동작 정책]이 기재되어 있었다.

21가지의 크고 작은 기능과 함께 9개의 컴포넌트로 나뉘어졌고 팀원들과 각각 역할 분배를 했다.

아래의 전체적인 개발 순서는 팀원들과 프로젝트의 우선순위 및 진행 방향의 주관적인 의견을 공유 하기 위해서 작성한 순서이다.

프로젝트의 volume이 크다고 생각되어 선택과 집중이 필요해보였다. 그래서 우선순위를 크게는 다음과 같이 작성했다.

  1. 기능구현
  2. CSS (점수 배점이 없지만 아에 안하기에는 👀)
  3. 리팩토링

👨🏻‍💻 전체적인 개발 순서

0. 프로젝트 세팅

  • CRA + ESLint & Prettier, Airbnb Style Guide

  • 절대경로

1. 컴포넌트 기능 구현

  • 컴포넌트간 구별을 위한 inline style (border) 등만 하기

  • 개발후 pull request 날리기

2. 최상단 App.js 에서 모든 state 취합

  • 옵션) input validation 체크 (?)

3. CSS 스타일링

- 아무리 css 안본다고 해도 그래도 봐줄만 한게 리팩토링보다 우선이라 생각합니다.

4. 리팩토링

- 과연 리팩토링 할 수있을까요 ...ㅎㅎ

⚙️ 구현한 기능 설명

1. 상품 정보 고시


( 구현화면 캡쳐 - 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);
    });
  };

2. 상품 소개 이미지, 구매자 추천 이미지


이미지 추가시 오른쪽에 이미지 이름과 함께 삭제를 할 수 있는 버튼이 노출된다.

아래는 이미지 추가 버튼을 누른 후에 일어나는 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); // 파일 경로 
  };

3. 상품 기본 정보

3.1 필터태그

필터 태그는 미리 지정되어진 리스트 중에서, 사용자에게 예시로 보여준다.

사용자는 원하는 필터 태그를 검색하거나 예시의 필터태그를 클릭해서 원하는 필터 태그를 지정한다.

  • 검색어 텍스트 일치(’자’ 기준)’가 높은 순으로 리스팅

  • 일치율이 동일한 경우, 서버에서 내려주는 데이터 순으로 노출

  • 검색 결과가 없는 경우 ‘검색 결과 없음' 안내

  // 검색 알고리즘
  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);
  };

3.2 상품명, 상품코드

상품코드는 임의로 만들어야 해서 많이 실제로 많이 사용하는 느낌인 영어와 숫자를 결합한 코드를 만들기로 했다. (9자리 * 16진수 => 16^9(6경?) 개의 상품 코드 생성 가능)

const productCode = Math.floor(Math.random() * 10000000000)
  .toString(16)
  .toUpperCase();

4. 저장하기 (console.log 출력)

모든 폼에서 작성된 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를 도움 받았고, 리팩토링을 진행하지 못한 것이 아쉽다. 역량을 끌어올려서 다음과제는 완벽하게 해내고 싶다.

추가로 소소하게 시간이 뺏길일이 있었는데 그 경험을 적어서 나중에 같은 실수를 반복하지 않고자 한다.

  1. 컴포넌트는 항상 대문자로 시작 (이거 관련해서 에러도 없다 🤬)

    • filterSearch.jsx (x) -> FilterSeach.jsx (o)
  2. onFocus이벤트의 반대는 OnBlur다

profile
느리지만 꾸준하게 💪🏻

0개의 댓글