본문 바로가기

나야, 리액트 스터디

[week5] 선언적 방식, 불필요한 상태 없애기, key를 이용해 state 초기화하기, Props를 state에 미러링X

안녕하세요🌊 웹파트 OB 김건휘입니다. 이번 시간에는 리액트 공식문서의 State 관리하기하기 파트 중 선언적 방식, 불필요한 상태 없애기, key를 이용해 state 초기화하기에 대한 저의 경험을 바탕으로 이야기해보는 시간을 갖도록 하겠습니다.

 

📌State를 사용해 Input 다루기

해당 챕터의 핵심 내용은 "선언적 방식으로 UI 설계"라고 할 수 있는데요. 

 

🧐선언적 방식이 뭔데??

React는 선언적인 프로그래밍(declarative programming)을 핵심 철학으로 삼고 있다. 이는 "어떻게(how)" UI를 구현할지 상세히 설명하기보다는, "무엇(what)"을 보여줘야 하는지에 집중하는 방식이라고 할 수 있다.

 

✅선언적 방식 vs 명령적 방식

명령적 방식 (Imperative)

UI를 구성하는 과정에서 일련의 작업과 단계를 세세히 지시한다.

const button = document.createElement('button');
button.innerText = 'Click Me';
button.addEventListener('click', () => {
  const heading = document.createElement('h1');
  heading.innerText = 'Hello World';
  document.body.appendChild(heading);
});
document.body.appendChild(button);

2주차 과제 때 겪어보았던, Vanilla JavaScript 코드이다.

 

  • 상태 변화나 DOM 조작의 세부적인 논리를 직접 작성하고 있다.
  • 무엇보다도 "어떻게(How)"를 중점으로 코드가 작성되어있다.

선언적 방식 (Declarative)

상태(state)에 따라 UI가 자동으로 갱신되도록 코드를 작성한다.

import { useState } from 'react';

function App() {
  const [showText, setShowText] = useState(false);

  return (
    <div>
      <button onClick={() => setShowText(true)}>Click Me</button>
      {showText && <h1>Hello World</h1>}
    </div>
  );
}

 

 

 

우리들이 열심히 공부하고 있는 React 코드이다.

  • UI는 상태를 기반으로 기술한다.
  • "무엇(What)"을 보여줄지에 중점을 두어 코드를 작성한다.

리액트에서는 잘짠 코드(가독성 높은, 필요한 상태만 관리하는)를 작성하기 위해서는 선언적 사고로 문제를 접근하는 것이 중요하다.

 

React에서 "어떻게"를 고민하는 대신, 다음과 같은 질문을 던지는 것이 선언적 사고의 핵심이라고 할 수 있다.

  1. 이 상태에서 무엇이 보여야 하는가?
  2. 이 컴포넌트는 어떤 데이터를 기반으로 동작해야 하는가?
  3. 어떤 이벤트가 발생하면 상태를 변경할 것인가?

React에서 UI를 선언적으로 설계한다는 것은 "결과를 정의하고, 구현은 React에게 맡긴다"는 의미가 될 수 있다. React 덕분에 우리들은 더 적은 코드로 더 많은 기능을 직관적으로 구현할 수 있다. React 감사합니다.

 

🚨버그와 모순을 피하려면 불필요한 state를 제거하세요

앱잼을 기준으로, 앱잼이 끝나고 리팩토링 단계에서 가장 우선적으로 하게 되는 과정 중 하나는 상태 재설계가 될 것이다. 앱잼 과정에서 무분별하게 정의해놓은 상태들이 리팩토링 단계를 거치게 되면 많이 축소 되는 경험을 하게된다. 공식문서에서도 "버그와 모순을 피하려면 불피요한 state를 제거하세요"라고 언급하고 있다. 나도 똑같은 경험을 했다. 해당 경험을 공유해보겠다.

리팩토링 전단계 코드

 

 

 

"기술 면접 준비" - targetName, "취업 준비" - targetCategoryName이 리팩토링 전 단계에서는 상태로 관리가 되고있었다. 그러나 상태로 관리 될 필요가 없다.

 

해당 부분이 왜? 상태로 관리 될 필요가 없는지 한번 생각해보고 다음 내용을 읽어보면 더욱 좋을 것이다.

 

🧐내가 처음에 targetName, targetCategoryName, targetime을 상태로 관리한 이유

첨부한 동영상과 같이 targetName,targetCategoryName이 사용자가 클릭하는 "할 일 카드"에 따라서 UI에 렌더링 해주어야한다. => "사용자 상호작용으로 인해 변경되는 값은 상태로 관리하라고 했으니, 당연히 상태로 관리해야겠네!"라고 생각했었다.

🥲targetName, targetCategoryName, targetime을 상태로 관리할 필요가 없는 이유

targetName,targetCategoryName은 데이터가 API를 통해 가지고 오는 것은 맞지만, 해당 페이지에서는 targetName,targetCategoryName이 수정될 일이 없으며, 단순히 화면에 띄워주는 역할만 수행하면 된다는 것이다. 이 말은 즉, 상태로 관리할 필요가 없으며 변수로 정의해서 필요한 컴포넌트에 props로 데이터를 넘겨주면 된다는 말이다.

 

상태 설계 단계에서 이 상태가 꼭 필요할까? 고민해보고 최소한의 상태를 사용하도록 하는 것이 바람직하다.

 

📌key를 이용하면 useEffect를 피할 수 있다

useEffect를 남용해서는 안된다는 사실은 나리스 스터디원이라면 매우 잘알고 있는 내용일 것이라고 생각하여 해당 내용의 설명은 넘어가겠다. 공식문서 "state를 보존하고 초기화하기" 챕터에서 "key를 이용해 state를 초기화" 하는 방법을 소개해주고 있다. 공식문서에서는 언급하고 있지 않지만, 해당 방법을 활용한다면 useEffect를 사용해서 구현해야하는 상황에서 손쉽게 문제를 해결할 수 있는 방법이 있어서 3주차 과제를 예시로 소개하고자 한다.

공식문서 내용

 

level 변경에 따른 게임보드 생성

 

 

레벨의 변경에 따라서 3x3, 4x4,5x5 게임보드가 렌더링 되어야한다. 첫번째 생각해볼 수 있는 해결 방법은 useEffect를 사용해 gridLength(레벨에 따른 게임보드 길이)가 변경될 때마다 resetGame을 호출하여, level 변경 시 GameBoard가 새로운 숫자 배열과 설정으로 초기화할 수 있도록하는 방법이 있을 수 있다. => 실제로도 많은 파트원들이 해당 방식으로 구현을 하였다.

그러나, 리액트 공식문서에서 언급하고 있는 key를 이용한다면 해당 상황을 useEffect없이 손쉽게 구현할 수 있다.

 

<GameBoard
            key={level}
            currentTime={formatTime(time)}
            onFirstClick={startTimer}
            onLastClick={handleGameEnd}
            onReset={resetTimer}
            {...getGameBoardProps(level)}
          />

다음과 같이 level을 최상단 컴포넌트에서 관리하도록 하고,  key={level} 속성을 GameBoard에 추가하여 level이 변경될 때마다 GameBoard가 재생성될 수 있도록하면 손쉽게 useEffect 사용없이 state를 초기화 해줄 수 있다.

 

 

📌Props를 state에 미러링하면 안되는 이유

공식문서 State 구조 선택하기 챕터

✅예시 코드

function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);


위의 코드에서 color state 변수는 messageColor prop로 초기화되고 있다. 문제는 부모 컴포넌트가 나중에 다른 값의 messageColor를 전달한다면 (예를 들어, 'blue' 대신 'red'), color state 변수 가 업데이트되지 않는다! 왜냐하면, State는 첫 번째 렌더링 중에만 초기화되기 때문이다.

그 때문에 state 변수의 일부 prop를 “미러링”하면 혼란이 발생할 수 있다. 대신 코드에 messageColor prop를 직접 사용하자. 더 짧은 이름을 지정하려면 상수를 사용하자.

 

function Message({ messageColor }) {
  const color = messageColor;


이렇게 하면 부모 컴포넌트에서 전달된 prop와 동기화를 잃지 않는다.

=> Props를 상태로 “미러링”하는 것은 특정 prop에 대한 모든 업데이트를 무시하기를 원할 때에만 의미가있다. props 데이터를 상태의 초기값으로 사용하는 패턴은 사용 가능하지만, 올바르게 사용해야 불필요한 혼란이나 문제를 피할 수 있다.

 

🧐궁금한점

더보기

프로젝트를 진행하다 보면, 최상단 컴포넌트에 너무 많은 상태가 관리 되고 있는 경우가 많이 발생하는데 이러한 경우에 전역상태라이브러리를 고려하게 되곤한다. 전역상태라이브러리는 꼭 필요한 경우가 아니라면, 사용하지 않는 것이 좋은 것으로 알고 있는데 전역상태라이브러리를 도입하는 기준이 다들 어떻게 되는지 궁금합니다!