[Front-end] 개발자 공부

[개발 공부 75일차] Managing State | React 공식 문서 Study

MOLLY_ 2024. 7. 1. 19:14
728x90

< 목차 >
1. 노베이스에서 토스 합격까지 (출처: Eddy)
2. Managing State  |  Reacting to Input with State
3. Managing State  |  Choosing the State Structure
4. Managing State  |  Sharing State Between Components
5. 금일 소감

 

 

1. 노베이스에서 토스 합격까지 (출처: Eddy)

 

https://velog.io/@eddy_song/retrospective

 

위 글이 꽤 와닿아서 갖고 와봤다. 위 사진은 내가 전에 부트캠프에서 프로그래밍을 배우다가 그만둬야 하나 싶은 생각이 들던, 한창 힘들 때 봤던 내용의 그림이다. 유명한 글이긴 하나, 링크 속 블로그에서도 그 내용이 나와있어서 좀 놀랐다.

 

글 요약을 할까 하다가 안 하려고 한다.

나중에 생각나서 볼 때, 내가 정리한 걸 보는 것보다 본문을 읽는 게 그 당시의 감정과 느낌이 고스란히 다시 느껴져서 본문을 들어가서 보게 되길래 이번엔 요약을 안 하려고 한다.

 

 

 

2. Managing State  |  Reacting to Input with State

0. TL;DR

  • 선언형 프로그래밍: UI를 세밀하게 직접 조작하는 것(명령형)이 아니라 각각의 시각적 state로 UI를 묘사하는 것
  • 컴포넌트를 개발할 때
    1. 모든 시각적 state를 확인하자
    2. 휴먼이나 컴퓨터가 state 변화를 어떻게 트리거 하는지 알아내자
      1. 버튼을 누르거나, 필드를 입력하거나, 링크를 이동하는 것 등의 휴먼 인풋
      2. 네트워크 응답이 오거나, 타임아웃이 되거나, 이미지를 로딩하거나 하는 등의 컴퓨터 인풋
    3. useState로 state를 모델링하자
    4. 버그와 모순(ex. 여러 state가 함께 False가 되거나 True가 되는 것)을 피하려면 불필요한 state를 제거하자
    5. state 설정을 위해 이벤트 핸들러를 연결하자

 

 

1. 무엇이 state 변화를 트리거하는지 알아내기

 

두 종류의 input 유형으로 state 변경을 트리거할 수 있다.

  1. 버튼을 누르거나, 필드를 입력하거나, 링크를 이동하는 것 등의 휴먼 인풋
  2. 네트워크 응답이 오거나, 타임아웃이 되거나, 이미지를 로딩하거나 하는 등의 컴퓨터 인풋

2가지 경우 모두 UI를 업데이트하기 위해서는 state 변수를 설정해야 한다. 지금 만들고 있는 폼의 경우 몇 가지 입력에 따라 state를 변경해야 한다.

 

 

 

이와 같은 흐름은 위 사진처럼 시각화할 수 있다.

 

각각의 state를 라벨링된 원으로 그리고, 각각의 state 변화를 화살표로 이어보자.

이러한 과정을 통해 state 변화의 흐름을 파악할 수 있을 뿐 아니라 구현 전에 버그를 찾을 수도 있다.

 

 

2. 메모리의 state를 useState로 표현하기

useState를 사용하여 컴포넌트의 시각적 state를 표현해야 한다. 이 과정은 단순함이 핵심이다. 각각의 state는 “움직이는 조각”이다. 그리고 “움직이는 조각”은 적을수록 좋다. 복잡한 건 버그를 일으키기 마련이다.

 

 

예시

// 1. 먼저 반드시 필요한 state를 가지고 시작
const [answer, setAnswer] = useState(''); // 인풋의 응답은 반드시 저장해야 할 것이고
const [error, setError] = useState(null); // 최근에 발생한 오류도 있다면 저장해야 할 것

// 2. 필요하다고 나열했던 나머지 시각적 state도 추가
// 좋은 방법이 곧바로 떠오르지 않는다면 가능한 모든 시각적 state를 먼저 추가하는 방식으로 시작
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

// 처음으로 떠올린 생각이 최고의 방법은 아닐 수도 있음
// state를 리팩토링을 하는 것도 과정 중 하나니 괜찮음. 모르겠으면 일단 해보고 생각 ㄱㄱ

 

 

3. 불필요한 state 변수를 제거하기

코드를 작성하다 보면, state의 중복은 피하고 필수적인 state만 남겨두고 싶을 것이다.

 

state 구조를 리팩토링하는 데 시간을 조금만 투자하면 컴포넌트는 더 이해하기 쉬워질 것이고, 불필요한 중복은 줄어들 것이며 의도하지 않은 의미를 피할 수도 있을 것이다.

 

리팩토링의 목표는 ‘state가 사용자에게 유효한 UI를 보여주지 않는 경우를 방지’하는 것!

 

 

예시

// 동시에 true가 될 수 없는 state를 축약해서 → 3가지(7개에서 줄어든!) 필수 변수만 남게 됨
// isError는 error !== null로도 대신 확인할 수 있기 때문에 필요하지 않음

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

 

어느 하나를 지웠을 때 정상적으로 작동하지 않는다는 점에서 이것들이 모두 필수라는 것을 알 수 있다!

 

 

💡 Reducer를 사용하여 “불가능한” state 제거

state를 조금 더 정확하게 모델링하기 위해서는 리듀서로 분리하는 방법도 있다.

리듀서를 사용하면 여러 state 변수를 하나의 객체로 통합하고 관련된 모든 로직도 합칠 수 있다!

 

 

4. state 설정을 위해 이벤트 핸들러를 연결하기

마지막으로 state 변수를 설정하기 위해 이벤트 핸들러를 연결하자.

 

아래는 이벤트 핸들러까지 연결된 최종 form이다.

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // 네트워크에 접속한다고 가정해보자
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

 

이렇게 모든 인터랙션을 state로 표현하게 되면,

  • 이후에 새로운 시각적 state가 추가되더라도 기존의 로직이 손상되는 것을 막을 수 있다.
  • 인터랙션 자체의 로직을 변경하지 않고도 각각의 state에 표시되는 항목을 변경할 수 있다.

 

 

3. Managing State  |  Choosing the State Structure

다음으로 들어가기 전에 귀여운 햅삐캣 밈을 보자

 

State를 잘 구조화하면 수정과 디버깅이 햅삐한 컴포넌트와 지속적인 버그의 원인이 되는 컴포넌트의 차이를 만들 수 있다.

 

 

0. TL;DR

  • 만약 두 state 변수가 항상 함께 업데이트된다면, 하나로 합치는 것을 고려해 보자
  • 특별히 업데이트를 방지하려는 경우를 제외하고는 props를 state에 넣지 말자
  • 선택과 같은 UI 패턴의 경우, 객체 자체가 아닌 ID 또는 인덱스를 state에 유지하자
  • 깊게 중첩된 state를 업데이트하는 것이 복잡한 경우, 평탄하게 만들어 보자

 

 

1. State 구조화 원칙

상태를 갖는 구성요소를 작성할 때, 사용할 state 변수의 수와 데이터의 형태를 선택해야 한다.

 

  1. 연관된 state 그룹화
    : 2개 이상의 state 변수를 항상 동시에 업데이트한다면, 단일 state 변수로 병합하는 것을 고려하자
  2. State의 모순 피하기
    : 여러 state 조각이 서로 모순되고 “불일치”할 수 있는 방식으로 state를 구성하는 것은 실수가 발생할 여지를 만든다.
  3. 불필요한 state 피하기
  4. State의 중복 피하기
  5. 깊게 중첩된 state 피하기
    : 깊게 계층화된 state는 업데이트하기 쉽지 않다. 가능하면 state를 평탄한 방식으로 구성하는 것이 좋다.

 

목표는 <오류 없이 상태를 쉽게 업데이트하는 것> 이다.

State에서 불필요하고 중복된 데이터를 제거하면 모든 데이터 조각이 동기화 상태를 유지하는 데 도움이 된다. 이는 데이터베이스 엔지니어가 데이터베이스 구조를 “정규화”하여 버그 발생 가능성을 줄이는 것과 유사하다.

 

 

 

“당신의 state를 ‘가능한 한 단순하게’ 만들어야 한다. ‘더 단순하게’가 아니라.”
- 알베르트 아인슈타인

 

 

 

 

2. 연관된 state 그룹화하기

단일 state 변수와 다중 state 변수 사이에서 무엇을 사용할지 불확실한 경우가 있다.

// 이렇게 '단일 state 변수'로 해야 할까?
const [x, setX] = useState(0);
const [y, setY] = useState(0);

// 아니면 아래처럼 '다중 state 변수'로 해야 할까?
const [position, setPosition] = useState({ x: 0, y: 0 });

// 2개의 state 변수가 항상 함께 변경된다면, 단일 state 변수로 통합하는 것이 좋다.
const [position, setPosition] = useState({
    x: 0,
    y: 0
  });

 

데이터를 객체나 배열로 그룹화하는 또 다른 경우는 ‘필요한 state의 조각 수를 모를 때’다.

ex) 사용자가 커스텀 필드를 추가할 수 있는 양식이 있는 경우에 유용하다.

 

 

⚠️ State 변수가 객체인 경우에는 다른 필드를 명시적으로 복사하지 않고 하나의 필드만 업데이트할 수 없다는 것을 기억하자.

예를 들어, 위의 예제에서는 y 속성이 없기 때문이다. setPosition({ x: 100 })를 할 수 없다. 대신, x만 설정하려면 setPosition({ ...position, x: 100 })(**Spread 문법 사용)**을 하거나 2개의 state 변수로 나누고 setX(100)을 해야 한다.

 

 

3. State의 모순 피하기

컴포넌트가 복잡할수록 무슨 일이 일어났는지 이해하기가 어렵다. 다음의 예시를 봐보자

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    
    // setIsSent와 setIsSending을 함께 호출하는 것을 잊어버린 경우,
    // isSending과 isSent가 동시에 true인 상황에 처할 수 있다.
    setIsSending(false);
    setIsSent(true);
  }
  
  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }
  
  
  // ----------------- 옳은 예시 구분선 -------------------
  
  
  // isSending과 isSent는 동시에 true가 되어서는 안되기 때문에,
  // 이 두 변수를 'typing'(초깃값), 'sending', 'sent' 3가지 유효한 상태 중
  // 하나를 가질 수 있는 status state 변수로 대체하는 것이 좋다.
import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }

  // 가독성을 위해 몇 가지 상수를 선언할 수도 있다.
  // state 변수가 아니기 때문에, 위 잘못된 사례 같은 문제가 생길 우려가 없다.
  const isSending = status === 'sending';
  const isSent = status === 'sent';

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

 

 

 

4. 불필요한 state 피하기

// 렌더링 중에 컴포넌트의 props나 기존 state 변수에서 일부 정보를 계산할 수 있다면,
// 컴포넌트의 state에 해당 정보를 넣지 않아야 한다.

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  
  // 여기서 fullName은 불필요하다.
  // 렌더링 중에 항상 firstName과 lastName에서 fullName을 계산할 수 있으니 제거하자.
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }
  
  
  // ----------------- 옳은 예시 구분선 -------------------
  
  
import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // 여기에서, fullName은 state 변수가 아니다. 대신, 렌더링 중에 계산된다.
  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

 

 

Props를 state에 미러링하지 말자

다음은 불필요한 state의 일반적인 예시다.

// 문제는 부모 컴포넌트가 나중에 다른 값의 messageColor를 전달한다면
// (예를 들어, 'blue' 대신 'red'), color state 변수 가 업데이트되지 않는다!
function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);

// 이렇게 하면 부모 컴포넌트에서 전달된 prop와 동기화를 잃지 않는다.  
function Message({ messageColor }) {
  const color = messageColor;
  
  
// Props를 상태로 “미러링”하는 건, 특정 prop에 대한 모든 업데이트를 무시하기를 원할 때에만
// 의미가 있다. 관례에 따라 prop의 이름을 initial 또는 default로 시작하여
// 새로운 값이 무시됨을 명확히 하자.
function Message({ initialColor }) {
  // prop의 추가 변경은 무시된다.
  const [color, setColor] = useState(initialColor);

 

 

5. State의 중복 피하기

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  
  // selectedItem의 내용이 items 목록 내의 항목 중 하나와 동일한 객체다.
  // 항목 자체에 대한 정보가 2곳에서 중복되고 있다.
  // (1) items = [{ id: 0, title: 'pretzels'}, ...]
	// (2) selectedItem = {id: 0, title: 'pretzels'}
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

// ----------------- 옳은 예시 구분선 -------------------

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
	// selectedItem 객체(items 내부의 객체와 중복을 생성하는) 대신 selectedId를
	// state로 유지하고, 그다음 items 배열에서 해당 ID의 항목을 검색하여
	// selectedItem을 가져온다.
	
	// 아래와 같이 변경되어 중복이 사라졌고, 필수적인 state만 유지된다.
	// (1) items = [{ id: 0, title: 'pretzels'}, ...]
	// (2) selectedId = 0
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

	const selectedItem = items.find(item =>
    item.id === selectedId
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

 

 

6. 깊게 중첩된 state 피하기

만일 state를 쉽게 업데이트하기 어렵게 너무 중첩되어 있다면, “평탄”(“정규화”라고도 함)하게 만드는 것을 고려하자.중첩된 항목을 업데이트하는 것이 더 쉬워졌다.

 

평탄화는 State를 업데이트하기 쉽게 만들고, 중첩된 객체의 다른 부분에 중복이 없도록 도와준다.

 

export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, {
      
 
// ----------------- 옳은 예시 구분선 -------------------


export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  },

 

 

 

4. Managing State  |  Sharing State Between Components

두 컴포넌트의 state가 항상 함께 변경되기를 원할 수 있다. 그렇게 하려면,

  1. 각 컴포넌트에서 state를 제거
  2. 가장 가까운 공통의 부모 컴포넌트로 옮긴 후
  3. props로 전달해야 한다.

⇒ 이 방법을 “State 끌어올리기”라고 하며 React 코드를 작성할 때 가장 흔히 하는 일 중 하나다.

 

 

0. TL;DR

  1. <State 끌어올리는 방법>
    1. 두 컴포넌트를 조정하고 싶을 때, state를 그들의 공통 부모로 이동한다.
    2. 그리고 공통 부모로부터 props를 통해 정보를 전달한다.
    3. 마지막으로 이벤트 핸들러를 전달해 자식에서 부모의 state를 변경할 수 있도록 한다.
  2. 컴포넌트를 (props로부터) “제어”할지 (state로부터) “비제어”할지 고려하면 유용하다.

 

 

1. State 끌어올리기 예제

import { useState } from 'react';

function Panel({ title, children }) {

  // 각 Panel 컴포넌트는 콘텐츠 표시 여부를 결정하는 Boolean형 isActive 상태를 가진다.
  const [isActive, setIsActive] = useState(false);
  
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="About">
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel title="Etymology">
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

 

 

한 패널의 버튼을 눌러도 다른 패널에는 영향을 미치지 않고 독립적으로 작동한다.

 

 

 

💡 그럼, 한 번에 하나의 패널만 열리도록 변경하려고 할 땐 어떻게 해야 할까?

⇒ 두 패널을 조정하려면 다음의 3단계를 통해 부모 컴포넌트로 패널의 “State 끌어올리기”가 필요하다.

  1. 자식 컴포넌트의 state를 제거한다.
  2. 하드 코딩된 값을 공통의 부모로부터 전달한다.
  3. 공통의 부모에 state를 추가하고 이벤트 핸들러와 함께 전달한다.

 

 

‘State 끌어올리기’ 과정

// Step 1: 자식 컴포넌트에서 state 제거하기
// 아래 줄을 Panel 컴포넌트에서 제거하는 것으로 시작한다.
const [isActive, setIsActive] = useState(false);

// 대신, Panel의 prop 목록에 isActive를 추가한다.
function Panel({ title, children, isActive }) {

// 이제 Panel의 부모 컴포넌트는 'props 내려주기'를 통해 isActive를 제어할 수 있다.
// 반대로 Panel 컴포넌트는 isActive를 제어할 수 없다.

// Step 2: 하드 코딩된 데이터를 부모 컴포넌트로 전달하기 
// state를 올리려면, 조정하려는 두 자식 컴포넌트의 가장 가까운 공통 부모 컴포넌트에 두어야 한다.
import { useState } from 'react';

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      
      // Accordion(가장 가까운 공통 부모) 컴포넌트가 하드 코딩된 값을 가지는 isActive를
			// (예를 들면 true) 두 패널에 전달하도록 만든다.
			
      <Panel title="About" isActive={true}>
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel title="Etymology" isActive={true}>
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

function Panel({ title, children, isActive }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}

// Step 3: 공통 부모에 state 추가하기
// 이 케이스에서는, 한 번에 하나의 패널만 활성화되어야 한다.
// 이를 위해 공통 부모 컴포넌트인 Accordian은 어떤 패널이 활성화된 패널인지 추적해야 한다.
// state 변수에 boolean 값을 사용하는 대신, 활성화되어있는 Panel의 인덱스 숫자를 사용할 수 있다.

// activeIndex가 0이면 첫 번째 패널이 활성화된 것이고, 1이면 두 번째 패널이 활성화된 것
const [activeIndex, setActiveIndex] = useState(0);

// 각 Panel에서 “Show 버튼을 클릭하면 Accordion의 활성화된 인덱스를 변경해야 한다.
// Accordion 컴포넌트는 Panel 컴포넌트가 state를 변경할 수 있음을
// '이벤트 핸들러를 prop으로 전달하기'를 통해 명시적으로 허용해야 한다.
<>
  <Panel
    isActive={activeIndex === 0}
    onShow={() => setActiveIndex(0)}
  >
    ...
  </Panel>
  <Panel
    isActive={activeIndex === 1}
    onShow={() => setActiveIndex(1)}
  >
    ...
  </Panel>
</>

 

 

완성된 ‘State 끌어올리기’

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel
        title="Etymology"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Show
        </button>
      )}
    </section>
  );
}

 

state를 공통 부모 컴포넌트로 옮기는 것은 두 패널을 조정할 수 있게 한다.

 

 

 

2. 제어와 비제어 컴포넌트

“제어되지 않은” 몇몇 지역 state를 갖는 컴포넌트를 사용하는 것은 흔한 일이다.

 

컴포넌트의 중요한 정보가 자체 지역 state 대신 props에 의해 만들어지는 경우, 컴포넌트가 “제어된다”고 한다. 이를 통해 부모 컴포넌트가 동작을 완전히 지정할 수 있다. isActive prop을 갖는 최종 Panel 컴포넌트는 Accordion 컴포넌트에 의해 제어된다.

 

비제어 컴포넌트는 설정할 것이 적어 부모 컴포넌트에서 사용하기 더 쉽다. 다만 여러 컴포넌트를 함께 조정해야 할 땐 비제어 컴포넌트가 덜 유연하다.

 

“제어”와 “비제어”는 엄격한 기술 용어가 아니며 일반적으로 컴포넌트는 지역 state와 props를 혼합해서 사용한다. 이런 구분은 컴포넌트의 설계와 제공하는 기능에 관해 설명하는 데 유용한 방법이다.

 

컴포넌트를 작성할 때 어떤 정보가 (props를 통해) 제어되어야 하고, 어떤 정보가 (state를 통해) 제어되지 않아야 하는지 고려하자.

 

 

3. 각 state의 단일 진리의 원천

각각의 고유한 state에 대해 어떤 컴포넌트가 “소유”할지 고를 수 있다.

 

이 원칙은 또한 “단일 진리의 원천(Single source of truth)” 을 갖는 것으로 알려져 있다. 이는 모든 state가 한 곳에 존재한다는 의미가 아니라 ‘그 정보를 가지고 있는 특정 컴포넌트가 있다’는 것을 말한다.

 

컴포넌트 간의 공유된 state를 중복하는 대신, 그들의 공통 부모로 끌어올리고 필요한 자식에게 전달하라.

 

 

 

5. 금일 소감

정리하며 읽다 보면 요약이나 정리에만 매몰되어 읽게 되기도 하는 것 같다. 그래서 의식적으로 내가 이해하기 쉽게끔 설명한다는 느낌으로, 좀 더 주체적으로 작성해봤다. 그러니 좀 더 내용이 더 와닿고 이해도 잘 되는 것 같다.

 

공식 문서는 확실히 어려운 부분도 있지만 새롭게 알게 되는 부분, 한 번에 읽히진 않지만 여러 번 다시 곱씹으며 읽으니 이해가 되는 부분이 있어 읽다 보면 정말 시간 가는 줄 모르고 읽게 되는 듯하다.

 

부트캠프 중반까지였을까. 그때 공식 문서 읽을 땐 '이게 뭔 소린가..' 싶어서 블로그나 증빙되지 않은 문서 위주로 참고를 많이 하였다. 그러다 사례가 별로 없는 오류의 디버깅을 하며 공식 문서에서 해답을 찾게 되는 게 한두 번 반복되니 공식 문서의 중요함 및 소중함을 크게 깨닫게 되었다.

 

공식 문서의 중요성은 이외에도 많지만 위 내용을 요약하며 다시 한번 든 생각이다.

 

 

추가로, 이건 React 공식 문서를 읽다가 아무 텍스트를 작성하지 않았을 때, Submit 버튼을 비활성화하는 코드가 있어서 이런 기능 추가하면 좋겠다 싶어서 바로 캡처했다.

 

스터디는 매주 토요일에 진행해왔는데, 이번 주 토요일이면 React 공식 문서의 마지막 챕터까지 다 읽게 되어 끝난다! 다음은 어떤 주제로 할지 정하지 않았지만 새로운 걸 시작할 생각에 약간 들뜬다. 새로운 걸 알게 되거나 하게 될 땐 기대도 되고 재미를 크게 느끼는 것 같다.

 

이제 짬짬이 내가 담당하지 않은 파트도 읽고, 기억해야 할 부분은 정리해놔야겠다!

 

728x90