어느새 항해를 진행한지 2달이 경과했다.
본 커리큘럼에 2/3 지점까지 지난 것인데,
앞에 써왔던 TIL, WIL을 훑어보면 부끄러워 질 정도로 허술하게 보이기도 한다.
그만큼 단기간에 많은 내용을 배우고 있는데,
공부하면서, 정리하고, 프로젝트를 진행하는 것이 생각보다 쉽지 않다.
외출한게 언제인지… 기억이 나지 않는다.

이번주의 나는?

저번주에 이어 연속되는 프로젝트 주간을 보냈다.
이번주의 주제는 클론 코딩 웹 사이트 한 곳을 선정하여 기능과 뷰를 똑같이 만들어 보는 작업이었다.

열정은 저번주 못지 않았지만… 첫주에 힘을 너무 뺀 탓일까
바로 이어지는 클론 프로젝트는 시작부터 컨디션이 좋지 않았다.
실제로 진행중 반나절 정도 기절하기도 했다…
항해를 진행하면서 체력관리의 필요성은 계속 느끼지만
막상 주간에 주어진 과제를 해결 하면서 조절하기가 쉽지 않다.

여기어때.

클론 코딩할 웹 사이트로 여기어때를 선정했다. 팀 내에서 회의를 하면서, 이번주에는 이미지 업로드, 위치기반 서비스를 구현해보고 싶다는 쪽으로 의견이 모아졌기 때문이다.
제시된 아이디어중 위 조건들에 부합하는 사이트를 선정했다.

와이어 프레임

금주에는 와이어 프레임에 저번주만큼 적극적이지 못했다..
클론 코딩이 생소했고, 어떻게 와이어 프레임을 짜야할지 감이 잘 서지 않아
사이트를 캡쳐해와 페이지간 연결, 구현할 부분에 대한 표시 정도로
간소하게 진행했다.

그라운드 룰

다들 전주 프로젝트를 경험해보면서 어느정도 협업하는 방법을 잡아가고 있었는데,
이번주 시작전 미니프로젝트 팀원이 공유해준 노션 템플릿을 바로 이용하면서
부가적인 시간을 줄이고 빠르게 API 테이블을 설계하는 등 큰 도움을 받았다.
다만… 기운이 빠져서 인지 저번주보다 노션을 많이 활용하지 못한점이 아쉽다.

내가 맡은 기능!

이번에도 Front 팀원과 파트를 나누는 중 둘 다
로그인/회원가입 기능을 구현해보지 않아 경험이 필요한 상태라 난감했다.
욕심나긴 했지만 로그인/회원가입을 넘기고
이미지 업로드 기능을 맡기로 협의했다.

작업을 하면서는 계속 스스로 고민에 빠졌는데,
클론 코딩인 만큼 뷰에 비중을 둬야할지,
기능 구현에 집중해야할지,
뷰를 잡아가다 보니 기능 구현이 뒤쳐지고,
기능에 집중하니 명색에 클론 코딩인데 뷰가 잘 잡히지 않았다. 중심을 딱 잡고 순차적으로 했으면 좋았을텐데
아직은 경험이 많이 부족한 것 같다

Css 요소는 이미 구현된 사이트를 보고 따라하는 것이기 때문에
크게 걱정하지 않았는데 오히려 자유 창작이던 전주보다
클론 해야할 요소를 똑같이 만들어가는게 더 어렵게 느껴지기도 했었다.

form-data

이번주에 가장 많은 고통과 고뇌를 준 키워드이다.
이미지를 업로드한다. 우리는 이미 일상속에서
숨쉬듯이 이용하고 있는 기능인지라 어렵게 생각하지 않았는데
처음 접하는 기능들은 어려울 수 밖에 없나보다..

Redux-toolkit을 사용하면서

// 게시물 작성 함수
export const __addProducts = createAsyncThunk(
  '__addProducts',
  async (payload, thunkAPI) => {
    try {
      await apis.post('/products', payload, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      })
      return thunkAPI.fulfillWithValue(payload)
    } catch (error) {
      return thunkAPI.rejectWithValue(error)
    }
  }
)

게시물 작성 함수에, form-data로 업로드 할 수 있는 코드를 넣어주고,
뷰 영역에서도 작성된 데이터를 받을 수 있도록

  // product 추가 함수
  const productButtonClickHandler = async (e) => {
    e.preventDefault()
    const formData = new FormData()
    formData.append('name', products.name)
    formData.append('star', products.star)
    formData.append('mainImage', products.mainImage)
    formData.append('address', products.address)
    formData.append('description', products.description)
    formData.append('ownerComment', products.ownerComment)
    formData.append('ProductType', 'Hotel')
    formData.append('roomList[0].roomName', products.roomList[0].roomName)
    formData.append('roomList[0].roomImage', products.roomList[0].roomImage)
    formData.append('roomList[0].roomPrice', Number(products.roomList[0].roomPrice))
    formData.append('roomList[1].roomName', products.roomList[1].roomName)
    formData.append('roomList[1].roomImage', products.roomList[1].roomImage)
    formData.append('roomList[1].roomPrice', Number(products.roomList[1].roomPrice))
    formData.append('roomList[2].roomName', products.roomList[2].roomName)
    formData.append('roomList[2].roomImage', products.roomList[2].roomImage)
    formData.append('roomList[2].roomPrice', Number(products.roomList[2].roomPrice))
    formData.append("category['spa']", products.category.spa)
    formData.append("category['miniBar']", products.category.miniBar)
    formData.append("category['wifi']", products.category.wifi)
    formData.append("category['bathItem']", products.category.bathItem)
    formData.append("category['tv']", products.category.tv)
    formData.append("category['airConditioner']", products.category.airConditioner)
    formData.append("category['refrigerator']", products.category.refrigerator)
    formData.append("category['showerRoom']", products.category.showerRoom)
    formData.append("category['tub']", products.category.tub)
    formData.append("category['dryer']", products.category.dryer)
    formData.append("category['iron']", products.category.iron)
    formData.append("category['electricRiceCooker']", products.category.electricRiceCooker)

    for (const [key, value] of formData.entries()) {
      console.log(key, value)
    } 
    // form-data로 들어오는 data를 확인

    dispatch(__addProducts(formData))
      .then(() => {
        alert('작성완료')
        navigate('/product')
      })
      .catch((error) => {
        alert('실패')
      })
  }

위와 같이 form-data 함수를 구성했다.
원래는 이렇게 많지 않았지만 후반 카테고리 체크 항목을 추가하면서
코드가 무시무시해졌다..
우리 팀의 initial 값은 아래와 같았는데,

const [products, setProducts] = useState({
    name: '',
    star: '',
    mainImage: null,
    address: '',
    description: '',
    ownerComment: '',
    productType: 'Hotel',
    roomList: [
      { roomName: ``, roomImage: null, roomPrice: `` },
      { roomName: ``, roomImage: null, roomPrice: `` },
      { roomName: ``, roomImage: null, roomPrice: `` },
    ],
    category: {
      spa: false,
      miniBar: false,
      wifi: false,
      bathItem: false,
      tv: false,
      airConditioner: false,
      refrigerator: false,
      showerRoom: false,
      tub: false,
      dryer: false,
      iron: false,
      electricRiceCooker: false,
    },
  })

formData.append() 를 작성하고 ()안에 값을 넣어주는 것은 어려운 것이 아니 었으나,
우리 팀이 고전했던 부분은 Front와 Back 사이에 정확히 key:value 값을 일치시켜야
서버에서 데이터를 받을 수 있기에 해당 부분을 맞추는 작업이 쉽지 않았다.

특히 객체 안에 배열 안에 객체(roomList), 객체 안에 객체(category)
이 경우 어떻게 key : value를 맞추어 작성할지 많은 고민의 시간이 필요했다.

특히 Front인 내 입장에서는 input 칸의 onChang를 어떻게 관리해야 할지
많은 고민이 필요했다.
이제까지는 객체 안에 배열, 객체가 있는 경우가 없었기 때문이다.

많은 고민과 검색 끝에 작성한 onChange 코드는 아래와 같다.

const changeInputHandler = (e) => { 
  const { value, name } = e.target
  // 이벤트 객체로부터 입력한 값과 인풋의 name 속성을 추출하여 상수에 저장
  if (name.startsWith('roomList')) {
    // 만약 인풋의 name 속성이 'roomList'로 시작한다면, 'roomList'를 가지는 배열의 항목을 수정하는 작업을 수행한다.
    const [, index, field] = name.match(/roomList\[(\d+)\]\.(.+)/)
    // 정규식을 이용해 'roomList' 배열의 인덱스와 필드 이름을 추출하여 상수에 저장한다.
    setProducts((old) => {
      // 기존의 state를 변경하기 위해 setProducts 함수를 사용한다.
      const newRoomList = [...old.roomList]
      // 기존 roomList 배열을 복사하여 새로운 변수에 저장한다.
      newRoomList[index][field] = value
      // 필드 이름과 인덱스를 이용하여 해당하는 항목의 값을 변경한다.
      return { ...old, roomList: newRoomList }
      // 변경된 roomList를 포함하여 새로운 state 객체를 반환한다.
    })
  } else {
    // 만약 인풋의 name 속성이 'roomList'로 시작하지 않는다면, 단순히 state의 값을 변경한다.
    setProducts((old) => {
      // 기존의 state를 변경하기 위해 setProducts 함수를 사용한다.
      return {
        ...old,
        [name]: value,
        // name 속성과 value 값을 이용하여 해당 프로퍼티의 값을 변경한다.
      }
    })
  }
}

코드만 봐도 어지러운데 나름 주석을 달아 놓으니 정신이 나갈 것 같다. 정규 표현식 부분만 더 살펴보면,

/roomList\[(\d+)\]\.(.+)/

roomList 문자열 다음에 “[” , 숫자 1개 이상, “]”
“.” 문자가 하나 이상 반복되는 문자열을 찾아 그룹화 하는 로직이다.

\[(\d+)\]

위 부분에서는 [] 사이에 숫자 1개 이상을 찾아서 첫 번째 그룹으로 저장한다.
즉, [숫자] 형태를 찾는 것이다.

\.(.+)

위 부분에서는 “.” 문자가 하나 이상 반복되는 문자열 다음에 오는 문자열을 찾아서
두번째 그룹으로 저장한다.
즉, “.”으로 시작하는 문자열에서 첫 번째 "." 이후에 문자열을 찾는 것이다.

initial 값에서 roomList[1].roomName을 위 로직으로 분해하면 아래와 같다.

  • “roomList[1].roomName”
  • “roomList[1]”
  • “1”
  • “roomName”

첫 번째 그룹에 숫자 1이 저장되고, 두 번째 그룹에는 문자열 “name”이 저장된다.
이를 이용해서 “roomList” 배열에서 해당하는 인덱스의 필드값을 추출할 수 있었다.

클론 프로젝트를 마치며

formData를 제외하고는 로그인 기능을 양보했기에,
CRUD 뿐이었기에 특별하게 기억에 남지는 않는다.
이번주에는 한 기능에 대해서 깊이 파고드는 경험을 했었는데,
많이 생각하고, 고민하고, 검색해본 내용이라 그런지 이렇게 WIL을 쓰는
순간에도 머릿속에 선명하다.

저번주에 이어 이번주도, 무리를 한 감이 있는데
다음주에는 드디어 6주간 진행하는 실전 프로젝트가 기다리고 있다.
부디 2달간 항해에서 배운 모든 역량을 펼칠 수 있길 바란다.

PS. 시간에 허덕여 컴포넌트 분리부터, 주석제거 코드를 정리할 수 있는
모든 부분이 엉망이다.. 실전 프로젝트 종료 이후,
시간을 내어 꼭 정리하도록 하자.

FE-Repositorie

Team Notion

카테고리:

업데이트:

댓글남기기