리액트로 개발한 첫 SPA

손윤주·2022년 5월 26일
0
post-thumbnail

👋 안녕 리액트!

오늘로 항해 21일차이다. 지난 일주일은 리액트 입문주차로, 간단한 기능을 담은 SPA를 개인과제로 제작해보았다.
SPA(Single Page Application)란 전체 페이지를 서버에서 매번 보내주는 MPA(Multi page Application) 방식이 아닌 브라우저단에서 자바스크립트가 관리하는 어플리케이션 이다.
리액트는 SPA의 대표적인 라이브러리인데, DOM을 조작해서 변경이 필요한 부분만 딱 변경할 수 있게 해준다!

DOM?

DOM은 프론트엔드 개발자라면 꼭 알고 넘어가야하는 중요한 개념이다.
MDN의 공식적인 정의는 HTML이나 XML문서를 브라우저에서 나타낼 수 있게 해주는 API이다.
다만 규모가 크고 복잡한 SPA에서는 데이터가 하나 수정될 때마다 DOM 전체가 리렌더링 되는게 매우 비효율적이기 때문에 가상돔을 활용한다.

가상돔(Virtual DOM)?

가상돔이란 실제돔의 복사본이자 추상화 버전이라고 볼 수 있다.
실제 돔의 odject와 같은 속성들을 가지고 있지만 실제 돔이 가지는 API는 가지고 있지 않다.
가상돔은 메모상에서 돌아가기 때문에 연산이 빠르고 가볍다. 따라서 가상 돔에서 바뀐 부분을 확인해서 최종적으로 실제 돔에 바뀐 부분만 찾아서 리렌더링할 수 있게되어 SPA의 성능을 확 끌어올려주는 감초같은 역할을 한다.

돌아오는 수요일에 DOM을 주제로 항해톡 발표를 자진해서😅 정신없는 와중에도 열심히 자료조사를 하고 있으니
DOM에 대한 내용은 항해톡 마무리 후에 여기에도 자세히 정리해서 업로드하고자 한다.

어쨋든 리액트SPA의 대표적인 라이브러리이자, 가상돔을 통해 업데이트를 관리한다!
리액트 첫인상은 .. HTML + CSS + 클래스형 컴포넌트 + 함수형 컴포넌트 를 JSX라는 문법으로 (자바스크립트 같으면서도 고유의 규칙이 따로 정해져있는 ㅠㅠ) 섞어 쓰는 짬뽕 🥘


😅 첫 개인과제를 진행하며

아주 진땀을 뺏다. 일주일 안에 리액트의 동작원리를 이해하고, 요구사항 기능을 구현한다는게 가능할지 걱정이 앞섰다. 왜냐면 클래스형 컴포넌트, 함수형 컴포넌트 둘 다 이해해야한다는게 머릿속을 아주 복잡하게 만들었다. 그래서 클래스형은 구조를 이해하는 정도로 넘어가고 함수형 컴포넌트를 집중적으로 파고들었다. 개인과제는 함수형 컴포넌트로만 작업을 진행했다. (리액트 공식 사이트에서 함수형 컴포넌트 사용을 공식적으로 권장함, but 클래스형을 사용하는 기존 프로젝트가 많기 때문에 이해하고 넘어가야 함)

추가로 이번에는 서버리스로 작업했다.

서버리스 란 서버를 관리할 필요 없이 애플리케이션을 빌드하고 실행할 수 있도록 하는 클라우드 네이티브 개발 모델입니다.

[개인과제 요구사항]

  • 내 일주일 평점 남기기 완성 + 파이어베이스 or S3로 배포 두 가지를 모두 완수해야 합니다.
    • 1) 메인 페이지
      • 일주일 평점 보여주기
        • 평점은 1~5까지 숫자 중 랜덤한 정수로 만들어주세요.
        • 각 요일 옆 삼각형 버튼을 누르면 요일 평점 남기기 페이지로 이동하기
    • 2) 평점 남기기 페이지
      • 선택한 요일 보여주기
      • 동그라미를 눌러서 평점 입력하기 (1번째 동그라미 누르면 1점, 3번째 동그라미 누르면 3점)
      • 남기기 버튼을 누르면 이전 페이지로 이동하기
    • 3) 평균 평점 보여주기 (메인 페이지 컴포넌트에 구성 or 하위 컴포넌트인 평균 평점 컴포넌트에 구성)
      • 각 요일별 랜덤 정수로 구성된 점수의 평균을 구하기
      • Reset 버튼을 누르면 평점 평균을 0으로 상태변화 시키기

위 과제를 구현하기 위해 몇가지 고민이 있었다.
구현과정에서 어떤 고민을 하고 어떻게 해결했는지를 기록하고자 한다.


컴포넌트를 어떻게 쪼갤까?

리액트는 컴포넌트들의 집합이다. 잘 만들어놓은 컴포넌트는 재사용이 가능하다.
먼저, 화면이 바뀌어도 고정으로 있을 메인 컴포넌트를 정하고 그 안에 들어갈 요소들을 쪼갰다.

App.js - 배경색 + 모바일사이즈의 흰색 박스
Home.js - 제목 + 요일별 평점과 버튼 + 평균 평점 + 리셋버튼
Review.js - 요일별 평점 남기기 UI
NotFound.js - 잘못된 주소로 접근 시 안내화면


요일별 평점을 랜덤으로 뿌려줘야 하는데..🤔

민망하지만 처음엔 일단 요일별로 하나씩 다 적어내렸다 😂
(사진은 당시 코드를 캡쳐해놓은게 없어서 같은 방식으로 작성했던 다른 분의 사진을 가져왔다.)

이렇게하면 랜덤 평점, 요일별 평점 기록하기 버튼도 7번씩 함수를 넣어줘야하는 번거로움도 있고 좋은 방법이 아니기에 요일+평점을 딕셔너리 형태로 만들고 map() 함수를 이용해서 (요일명 + 평점 + 평점남기기버튼)을 7번 반복하도록 돌려주었다.

  const Home = (props) => {
  let history = useHistory();
  const WeekDay = ["월", "화", "수", "목", "금", "토", "일"];
    
      let RateSum = 0;
    
      const WeekRate = WeekDay.map((day) => {
    const random = Math.floor(Math.random() * 5 + 1);
    RateSum += random;

    return {
      day: day,
      rate: random,
    };
  });
  
   return (
    <>
      <h2> 내 일주일은?</h2>
      {WeekRate.map((d, i) => {
        return (
          <Line key={i}>
            <h3>{d.day}</h3>
            {Array.from({ length: 5 }, (c, i) => {
              console.log({ avg });
              return (
                <Circle
                  key={i}
                  style={{
                    backgroundColor:
                      d.rate > i ? "#fff500" : "#ddd",
                  }}
                />
              );
            })}

            <Tri
              onClick={() => {
                history.push(`/review/${WeekDay[i]}`);
              }}
            >
              Go
            </Tri>
          </Line>
        );
      })}

요일명은 딕셔너리의 day 값으로 들어가고, 평점남기기 버튼 클릭시 해당요일의 상세페이지로 넘어가도록 useHistory hook을 사용해서 링크에 요일 리스트의 [i]번째가 들어가도록 넣어주었다. (이 때 백틱으로 링크를 감싸고 변수에 ${ } 를 감싸주어야하는 JSX 문법을 적용하는데에 시간을 많이 보냈다 😭)

랜덤 평점 구하기 🥳

const random = Math.floor(Math.random() * 5 + 1);

Math.random() 함수는 0부터 1 사이 숫자를 랜덤으로 뿌려주고, 여기에 5을 곱하면 4.xxxxxx..의 값이 나온다. 이 값에 +1을 해서 Math.floor() 함수로 소숫점을 버리면 1부터 5 사이의 숫자가 랜덤으로 나오게 된다. 이 random 값을 요일별 평점을 담은 딕셔너리의 평점 value 로 넣어주었다.

return {
      day: day,
      rate: random,
    };

랜덤평점에 따른 컬러 나타내기 🙂🙂🙂🫥🫥

 {Array.from({ length: 5 }, (c, i) => {
              console.log({ avg });
              return (
                <Circle
                  key={i}
                  style={{
                    backgroundColor:
                      d.rate > i ? "#fff500" : "#ddd",
                  }}
                />
              );
            })}

랜덤 평점이 3이라면 동그라미 5개 중에 3개는 채워진 노란색 동그라미로 구현해야했다.
이를 위해 length가 5만큼인 동그라미를 Array.from() 함수로 돌려주고
삼항조건 연산자를 사용해서 랜덤 평점이 동그라미의 i보다 크면 노란색, 적으면 회색을 보여주도록 했다.
이 때 회색 동그라미와 노란색 동그라미 각각의 변수를 만들지 않고 style={{ }} 안에 삼항연산자를 넣어 바로 컬러를 지정했다.


전체 요일의 평균평점 구하기

  let RateSum = 0;

  const WeekRate = WeekDay.map((day) => {
    const random = Math.floor(Math.random() * 5 + 1);
    RateSum += random;

    return {
      day: day,
      rate: random,
    };
  });

  const average = (RateSum / 7).toFixed(1);

전체요일의 평균평점은 빈 변수를 만들고 거기에 랜덤으로 도출한 요일별 평점을 모두 넣어준 후 7로 나누었다.
이 때 평균평점은 소숫점 1의 자리 수까지 나오도록 요구했으므로 .toFixed(1) 을 추가했다.

메인화면 UI


평점 남기기 페이지 UI

내가 선택한 동그라미까지 노란색을 채우는 법?

메인 페이지에서는 평점을 랜덤으로 뿌려줬지만,
평점 남기기 페이지에서는 해당 요일의 평점을 남기는 기능을 구현해야 했다.
이 때는 내가 클릭한 idx까지 동그라미가 노란색으로 채워지도록 접근했다.

const Review = (props) => {
  const [rate, setRate] = useState(-1);
  let history = useHistory();
  const week = useParams();

  function ReviewBtn() {
    history.push("/");
  }
  console.log(week);

  return (
    <>
      <ReviewText>
        <Day>{week.day}요일</Day>
        <Text>평점 남기기</Text>
      </ReviewText>
      <RatingBox>
        {Array.from([0, 1, 2, 3, 4], (score) => {
          return (
            <Circle
              key={score}
              style={{
                backgroundColor: rate >= score ? "#fff500" : "#ddd",
              }}
              onClick={() => {
                setRate(score);
              }}
            ></Circle>
          );
        })}
      </RatingBox>
      <Btn onClick={ReviewBtn}>평점 남기기</Btn>
    </>
  );
};

마찬가지로 Array.from() 으로 동그라미 5개를 만들고 그 중 동그라미 하나를 클릭하면 useState hook이 실행된다. 초깃값이 -1인 rate가 클릭한 동그라미의 idx값으로 업데이트되면서 업데이트된 rate 값이 배열 속 값보다 같거나 크면 노란색, 적으면 회색으로 나타난다.


평균평점 리셋시키기 🫥

메인 화면에서 리셋버튼 클릭시 전체요일의 평균평점을 0.0으로 리셋하는 기능이다.

  const average = (RateSum / 7).toFixed(1);
  const [avg, setAvg] = useState(average);
      <Btn
        onClick={() => {
          setAvg(parseInt(0).toFixed(1));
        }}
      >
        Reset
      </Btn>

요일별 랜덤평점을 모두 더한 값에 7을 나누고 소숫점 1의 자리 수까지 남긴 값을 average 변수에 넣어주었다. 그리고 리셋버튼 onClick 발생 시 useState hook 으로 average 값이 업데이트 된다. 이 때 average 초깃값이 문자열이므로 parseInt(0)로 0으로 만들어주고 toFixed(1) 를 통해 0.0으로 나타낼 수 있었다.


추가로 구현해 보았어요 😇

평균 평점을 리셋하면 0.0으로 바뀌는데, 요일별 랜덤 평점이 그대로 남아있는게 아무래도 어색해보였다.
그래서 평균평점 리셋 시 요일별 평점도 같이 리셋되도록 추가로 구현해보았다.
평균평점 변수는 위에서 선언해주었기 때문에 요일별 랜덤 평점에 평균평점이 0.0일 때 동그라미가 회색으로 보이도록 조건을 추가했다.

 {Array.from({ length: 5 }, (c, i) => {
              console.log({ avg });
              return (
                <Circle
                  key={i}
                  style={{
                    backgroundColor:
                      d.rate > i && avg !== "0.0" ? "#fff500" : "#ddd",
                  }}
                />
              );
            })}

삼항연산자를 중첩해서 사용할까 하다가, 더 간단한 방법이 떠올라서 기존 조건에 && avg !== "0.0" 을 추가했다. 이로써 평점이 i보다 크거나 전체평균 평점이 0.0이 아닐 때에 노란색으로 나타나게 되었다.
앱에서 큰 비중을 차지하는 리셋버튼 클릭 시 전체가 리셋되는게 사용자경험상 훨씬 자연스러워보인다.

그리고 아주 간단하지만 메인화면과 요일별 평점 남기기 url 외에 사용자가 잘못된 경로로 접속할 경우 빈 페이지를 띄워주지 않고 안내 페이지를 띄워주도록 설정했다. Switch 훅을 사용해서 지정된 경로 외에는 모두 위 UI를 띄워주도록 했다. 이런 작은 센스가 사용자 경험을 향상시킨다고 믿는다.

그렇게 완성된 첫 리액트 개인과제 🥳

'일주일 평점 남기기' 깃헙 링크

이번 과제를 통해 SPA의 장점을 몸소 느끼게 되었고 state 업데이트 시 리렌더링되는 리액트의 동작원리, 함수형 컴포넌트에서 사용하는 Hook (useHistory, uesState, Route, Switch) 을 직접 사용해볼 수 있었다. 또 컴포넌트를 쪼개서 원하는 부분만 조작하고, 실제 DOM에서 해당 부분만 변경되는 것을 확인해볼 수 있었다. 업데이트시 해당 컴포넌트 전체 리렌더링은 필수이지만 가상돔을 통해 메모리상에서 수정된 부분을 빠르게 확인하고, 최종적으로 바뀐 부분만 실제 DOM에 적용되기 때문에 가상돔 덕을 톡톡히 보는 SPA!

업데이트 시 리렌더링되는 SPA의 동작원리를 이해하고나니 앞으로는 단순 기능구현 뿐 아니라 제품의 성능향상도 같이 고려하면서 개발하고자 한다.

0개의 댓글