저번에 결국, DessertItem 컴포넌트와 선택하기 버튼을 따로 분리해서 만들기로 결정했다. 그래서 SelectButton이란 컴포넌트를 만들고 다음과 같이 App.js에서 사용하기로 했다.

return(
        <div>
            <div className="select-zone">
                {dessertList[selectIndex]}
                <SelectButton onSelect={selectDessert}/>
                {dessertList[selectIndex + 1]}
                <SelectButton onSelect={selectDessert}/>
            </div>
        </div>
    )

dessertList에서 selectIndex와 그 다음 번째의 디저트들을 보여주고, SelectButton도 같이 보여준다. 이 때 onSelect prop으로 저번에 작성한 selectDessert메소드를 넘겨준다.

 

그러나 지금까지의 selectDessert메소드는 단순히 selectIndex를 2씩 증가시키는 기능만 했다. 그러면 이제 이 메소드에 선택한 애들을 골라담아주는 기능도 넣어야 한다.


1. 선택한 애들 담기 -  파라미터가 있는 메소드를 prop으로 주는 법을 배우다

처음에 고민이 됐던 건,.선택하기 버튼을 분리한 마당에 특정 디저트를 어떻게 선택버튼과 매칭시켜서 담게 할 것인가였다. 그러다가 간단히 해결될거란 생각이 들었던게, selectDessert의 prop으로 특정 디저트들을 보내면 될 것 같았다. 선택된 디저트들을 담을 selectedList라는 state를 만들고, 선택하기 버튼이 눌릴 때마다 해당 디저트들(정확히는 DessertItem 컴포넌트)를 거기에 추가해준다! 따라서 selectDessert메소드는 선택된 디저트를 파라미터로 받아 다음과 같이 처리하도록 했다.

const selectDessert = (item) => {
        setSelectedList([...selectedList, item]);
        let nextSelectIndex = selectIndex + 2;
        setSelectIndex(nextSelectIndex);
    };

근데 여기서 작은 고민이 생겼다. 현재 App.js에서 SelectButton은 다음과 같이 쓰이는 상태다.

return(
        <div>
            <div className="select-zone">
                {dessertList[selectIndex]}
                <SelectButton onSelect={selectDessert}/>
                {dessertList[selectIndex + 1]}
                <SelectButton onSelect={selectDessert}/>
            </div>
        </div>
    )

onSelect라는 prop으로 selectDessert라는 메소드를 주는 모습. 그러나 내가 지금 원하는 건 onSelect라는 prop으로 "특정 디저트 아이템을 파라미터로 받는 selectDessert메소드"를 넘겨야 한다!! 근데 그렇다고 다음과 같이 수정하면 당연히 안 될 것이다.

<SelectButton onSelect={selectDessert(item)}/>

ㅋㅋㅋㅋㅋ 저렇게 되면 item을 인자로 쓰는 selectDessert메소드를 prop으로 넘기는 게 아니라 호출된 값?을 넘기는 식으로 쓰일 것이다. 내가 아무리 잘 몰라도 이 정도 상식은 있는 사람이다..와 이거 어떡하지? 어떻게 해야 하지? 라는 생각이 들었다. 모를 땐 역시 구글링이 답이라고, 검색해보니 금방 방법을 찾을 수 있었다.

 

우선, SelectButton의 prop으로는 selectDessert메소드에서 인자로 넘길 디저트와 selectDessert메소드를 따로따로 넘긴다.

<SelectButton item={dessertList[selectIndex]} onSelect={selectDessert}/>

그리고 이걸 SelectButton 컴포넌트에서 다음과 같이 익명함수를 활용하는 방식으로 짬뽕한다.

function SelectButton({item, onSelect}){
    return(
        <button onClick={() => {onSelect(item)}}>
            선택하기
        </button>
    )
}

export default SelectButton;

와 어떻게 이런 방법을..?ㅋㅋㅋㅋㅋㅋ 정말 간단하다. 암튼 이렇게 선택된 애들을 따로 담는 부분은 해결했다.

 

2. 라운드 시스템(16강, 8강, 4강) 구현 - "useEffect 이래서 쓴다"를 배우다

이제 라운드 시스템만 잘 만들면 대략적인 틀은 다 만들게 된다. 처음 16개 중 8개만 고르면 8강을 진행하고, 거기서 또 4개를 고르면 4강을 진행하고 이런 식으로 가야 한다.

어떻게할까..고민했다. 16개중 8개를 고르는 순간 selectIndex가 14에서 16으로 바뀌기 때문에, 16으로 바뀐 순간 selectedList를 dessertList에 덮어씌우기로 했다. 그래서 다음과 같이 코드를 짰는데, 뭔가 이상했다. 

console.log는 무시해주세요

roundOf는 state값으로 처음엔 16을 지닌다. setter함수(setSelectIndex)를 통해 selectIndex값을 2만큼 늘려주고 나서 selectIndex값이 16일 때의 동작을 정의한 것인데, 문제는 16이 된 순간에 바로 setter함수들이 동작하지 않는다는 것이었다. 뭔가 이상하다 싶어서 setSelectIndex 전후로 console에 selectIndex값을 찍어봤는데 같은 값이 나왔다. ...도대체 뭐지?

 

바로, 리액트의 setter함수는 비동기적으로 동작하기 때문이었다.

 

react는 state가 변경될 때마다 그 state를 갖는 컴포넌트를 새로 렌더링하는데, state를 바꾸는 작업이 한꺼번에 이루어지면 그만큼 많은 렌더링이 발생하므로 이를 방지하기 위해 일종의 대기열에 넣고 변경사항을 한꺼번에 적용한다. 식당을 예로 들면 여러 테이블로부터 주문이 들어올 때마다 그때그때 받는게 아니라 마지막 테이블이 주문할 때까지 다 기다리다가 한꺼번에 받는 식. 하나의 이벤트 핸들러에서 state변경이 여러번 이뤄진다면 이벤트가 끝날 시점에 state변경을 일괄적으로 진행한다고 한다.

 

따라서, 내가 한 방식처럼 같은 메소드에서 setter함수로 바꾸자마자 그 값의 변화에 따른 동작을 하는 것이 예기치 못한 오류?를 발생할 수 있었던 것.

 

해결하는 방법은 setter함수를 호출할 때 두번째 파라미터로 콜백함수를 넣는 것이었으나, 이 기능은 없어졌다고 한다. useEffect의 사용을 권장한다고 함!! 딱 구글링으로 이렇게 보자마자 무릎을 탁 쳤다. useEffect는 state가 변할 때마다 실행되도록 만들 수 있으니까!!

 

그래서 다음과 같이 작성해줬다.

useEffect(() => {
        if (selectIndex === roundOf){
            setDessertList(selectedList);
            setSelectedList([]);
            setSelectIndex(0);
            // parseInt : 몫만 취해준다.(소수점 밑 버리고 정수형 값만 취함)
            setRoundOf(parseInt(roundOf / 2));
        } 
    }, [selectIndex]);

 

구동모습(테스트 목적으로 구분선 아래로는 선택한 디저트들이 보이도록 함)

 


정말 작은 개인플젝?이지만..정말 하기 잘 했다는 생각이 든다. 이렇게 내가 직접 만들면서 느낀 건 단순히 강의나 책으로 보는 것보다 더 오래 뚜렷하게 기억에 남을 것 같다. 잘 마무리지어야겠다.

저번에 mock데이터들을 map메소드를 통해 가공한 애들을 들을 출력하는 것까지 완성했었다. 얘네들을 dessertList라는 배열에 담기로 했는데, 이 때 생각할 거리가 좀 있어서 다음과 같이 처리하기로 했다.

 

1. 일단 디저트들을 좀 섞는 게 어떤지? 

: 그냥 이대로 쓰면 매번 내 사이트를 들어올 때마다 같은 구성으로 16강이 진행되는(예를 들면 처음엔 마카롱은 항상 다쿠아즈와 경쟁하는) 문제가 있다. 따라서 이를 섞어야 할 필요가 있었다. 0이상 1미만의 난수를 랜덤 생성하는 Math.random과 콜백함수의 리턴값이 양수/음수/0인지를 보고 정렬하는 sort()를 이용해서

dessertList.sort(() => {return Math.random() - 0.5});

이렇게 할 수도 있었지만, 이 방법은 좋지 않다고 한다. 늘 고른? 분포를 보이진 않는 모양. 그러던 와중 피셔-예이츠 셔플?이 좋다고 해서 그 방식을 사용해 섞기로 했다.

// 피셔 - 예이츠 셔플(배열 섞는 알고리즘)
function shuffle(array) {
    for (let i = array.length - 1; i > 0; i--) {
        let j = Math.floor(Math.random() * (i + 1));
        // destructuring 문법
        [array[i], array[j]] = [array[j], array[i]];
    }
}

 

2. 16개 전부다 한꺼번에 출력하는게 아니라 2개씩 보여주고 싶다

당연히 16강이고, 2개씩 보이면서 하나씩 고르는 걸 내가 원했으니까..이걸 어떻게 할까 고민하다가, selectIndex라는 state를 만들어

 

1) dessertList의 selectIndex번째 디저트와 selectIndex + 1번째 디저트를 보여줌

2) 사용자가 '선택하기' 버튼을 누르면 selectIndex값이 2만큼 증가!

 

하는 식으로 만들기로 했다. 

그래서 DessertItem 컴포넌트에 선택하기 버튼을 추가해서 만들기로 하고, 이를 위해 App.js에서 selectIndex를 + 2만큼 증가해주는 메소드를 만든 후 이를 DessertItem의 prop으로 보내 onClick속성으로 활용키로 했다. 좋아좋아 아주 순조로워~~

 

정리하자면

 

mock데이터를 가공해 각각의 요소들을 JSX요소들로 만들고 이를 dessertList라는 배열에 담는 거였는데, 우선 JSX요소들로 만들어주기 전에 섞어줄거임(mock데이터 배열의 요소들을). 그리고 얘네들을 selectIndex라는 state를 활용해 2개씩 보여줄 예정이니까, mock데이터들을 섞은 후에 map메소드로 JSX요소들(정확히는 DessertItem 컴포넌트들)로 가공해줄 때 'selectIndex를 2씩 증가해주는 메소드를 onClick속성으로 갖는 버튼'을 갖게 한다!!

 

즉 웹사이트를 처음 열자마자 mock데이터에서 갖고 온 애들을 섞고, 버튼까지 딸린 컴포넌트들로 가공한 애들을 dessertList에 담아둬야 함. 근데 옛날에 useEffect를 사용하면 처음 렌더링될때만 어떤 동작을 할 수 있다고 어렴풋이 들었던 기억이 나서, 다음과 같이 useEffect를 이용하게 됐다.

console.log()는 그냥 테스트용으로 찍어보던 거라 무시바람..암튼 이렇게하면 아무 문제없을 줄 알았다.

 

그런데 오류가 났다.

React Hook useEffect has a missing dependency: 'selectNextDessert'. Either include it or remove the dependency array

 

selectNextDessert가 selectIndex라는 state값을 늘려주는 메소드였는데, 이거 땜에 문제가 생긴다고 한다.  useEffect의 두 번째 파라미터인 빈 배열 안에 selectIndex를 넣어주면 오류가 안 나긴 하지만 그렇게되면 selectIndex가 늘어날 때마다 shuffle이 계속 실행되는 불상사가 발생..[]자체를 없애면 렌더링될때마다 useEffect의 콜백함수가 실행되고..

 

구글링도 열심히 해보고 했지만 도무지 모르겠다. okky커뮤니티에도 상세하게 질문을 남겼지만 답변이 달리지 않았다. 내가 완전 기본적인 내용을 놓치고 있는 듯한..


# 뭐 어떻게든 해결은 했다만..

결국 통 모르겠으니, 특단의 조치를 내렸다. 선택하기 버튼 자체를 분리해서 만들기로 한 것! 이렇게하니까 오류는 안 생기긴 한다..

나중에 공부를 계속하며 해결될 문제라고 생각된다..화이팅하자..

 

PC방 알바하던 중이었다. 잠깐 시간이 비어서 카운터 컴퓨터로 유튜브를 켰는데 알고리즘으로 침착맨의 "가장 쉽게 질리는 음식 16강"이란 제목의 영상이 떴다. 영상을 보다가 "나도 한 번 16강 웹사이트를 만들어볼까?"라는 생각으로 시작하게 됐다.


어떤 식으로 할 건지 구상해봤다.

우선 처음 시작하면 16개의 아이템들이 랜덤하게 섞이고, 2개씩 사용자 화면에 출력된다. 사용자가 선택한 음식들은 별도의 배열에 담기고, 16개의 아이템에 대해 모두 셀렉이 끝나면 8강을 진행. 이후 4강 진행, 결승.

 

난이도가 있는 프로젝트는 아니라고 생각되지만, 강의 실습이 아니라 처음으로 스스로 하는 프로젝트가 될 것 같다. 작년 멋사에서 Django로 플젝할 때와는 다른 설렘이 생기는 듯. 역시 강의들으며 실습보단 스스로 뭔가 만드는 게 더 재밌는 건가 싶다 ㅋㅋ

 

일단 이 플젝은 내가 리액트로 처음 하는 개인 플젝이므로, 이 플젝으로 얻고 싶은 소소한 목표는 다음과 같다.

 

1. 기본적인 컴포넌트 다루는 법 익히기 

: 강의 실습이 아니라 내가 스스로 만들면서 하는 거니까 컴포넌트의 개념이 좀 더 와닿지 않을까 생각한다.

2. json파일 다루기

: 대부분의 웹사이트는 json배열 등을 가공하고 출력하고 뭐 그런다고 한다. 강의에서는 API로 데이터를 받아오기 전에 mock데이터(샘플로 쓸 데이터를 mock데이터라고 부른다고 하더라)를 만들어서 쓰던데, 나도 이 플젝을 통해 json파일 등을 다루는 데 좀 익숙해지면 좋겠다. 작년에 Django로 해커톤할때도 json을 다루는 일이 종종 있었기 때문..

3. API로 데이터를 받아오는 것 이해하기

: 사실 js를 공부하고 리액트를 시작한게 아니라 리액트와 js를 병행하면서 그 때 그 때 모르는 걸 찾아보는 식으로 하기 때문에 fetch, async, await, premise? 이런 거 잘 모른다. 이 플젝을 하면서 이런 게 뭔지 이해하자. 일단 처음엔 나도 mock.json만들고 다루다가 좀 가닥이 잡히면 API로 데이터 받아와보는 식으로 해야겠다.

 


# 일단 mock.json만들자!

일단 샘플 데이터들이 있는 mock.json을 만들기로 했다. 근데 시작부터 나름 큰 난관에 봉착.

 

"도대체 어떤 16강 월드컵을 만들지?"

 

킹받는 롤 챔피언 16강 만들까? 이런 거 고민하다가 카페에서 하던 중이라 그런지 갑자기 가장 먹고 싶은 디저트 16강으로 정했다.ㅋㅋㅋㅋㅋ

 

나름 샘플 만드는 과정도 재밌었다. 디저트를 뭘로 하지?부터 이미지는 다운받아서 로컬에 넣을까 하다가 그냥 구글에 검색해서 이미지 주소 복사해서 쓰기로 했다. 근데 이거 저작권에 문제 생기나..? 문제 생기면 프로젝트 폐기 이런건가..

 

mock.json은 다음과 같이 만들어졌다.

배열 형태고 각 원소?들은 id, name, imgUrl속성?을 갖도록. 이제 이 놈을 App.js에서 다음과 같이 임포트했다

import mock from "../mock.json";

리액트는 import를 활용해 모듈이나 파일을 추가하듯이 이미지를 추가할 수 있다.  리액트에서 로컬에 있는 이미지 등을 다룰 땐 저렇게 임포트해서 경로를 쓰는게 더 낫다 카더라. 아니면 오류가 난다꼬..json도 저렇게 import하면 mock이란 이름으로 쓸 수 있다.

 

이제 이 놈들을 시험 삼아서 화면에 띄워보기로 했다. 배열의 map메소드를 활용하면 배열의 각 아이템들에 대한 가공?이 가능하고, 이 map메소드 안에서 JSX요소를 리턴하면 마치 여러 개의 JSX요소를 추가한 것처럼 동작한다. App에서 다음과 같이 map메소드를 쓰면서 DessertItem 컴포넌트를 만들도록? 했다.

function App(){
    return(
        <div>
            {mock.map((dessert) => {
                return(
                    <li key={dessert.id}>
                        <DessertItem item={dessert}/>
                    </li>
                )
            })}
        </div>
    )
}

아! 그리고 리액트에서 배열을 다룰 때는 저렇게 원소들에 key속성을 넣어줘야 하고, 속성값으로는 각 요소를 구분지을 수 있는 값을 넣어야 한다.  공식 문서에 따르면 key는 리액트가 어떤 항목을 추가, 변경, 또는 삭제할지 식별하는 것을 돕는다고 함. 따라서 배열의 인덱스를 key로 쓰는 멍청한 짓은 하면 안 됨. 배열 요소들의 순서가 바뀔 수도 있으니까 인덱스는 고유한 값이 아니기 때문!

 

암튼 DessertItem는 다음과 같이 작성해 디저트 사진과 이름을 볼 수 있도록 했다.

function DessertItem({item}){
    return(
        <div>
            <h3>{item.name}</h3>
            <img src={item.imgUrl} alt="디저트 사진" />
        </div>
    )
}

이제 출력된 모습!

 

별 거 안했지만 그래도 혼자 해봤다는 게 조금 뿌듯..뿌듯할 레벨은 아니지만 ㅎㅎ..

암튼 이제 시작이다! 잘 만들어보자

 

+ Recent posts