[우아한 테크코스 #12] 👻 setState 👻로 돌아보는 12주차 회고록

NinjaJuunzzi·2022년 5월 15일
3

우아한테크코스

목록 보기
15/21
post-thumbnail
post-custom-banner

감정회고 & 페어회고

안녕하세요 준찌(혹은 찌구리, 준조림, 준구리)입니다. 이번 주차는 페어주차였습니다 !! 이번 페어 프로그래밍 간에는 페어노트를 활용해보았는데요. 저번 페어에서 느꼈던 프로젝트 매니징 영역에서의 문제를 해결하고자 제안해보았습니다.(이번 페어분이 문서화를 좋아하시는 분이기도 해서 ^^..)

위 템플릿을 활용하여 페어를 진행해보았는데 장점은

  • 각자 회고를 통해 감정을 정리해볼 수 있었어요!
  • TODO에 기록해두니 어떤 걸 하려했는지 까먹지 않고 개발할 수 있었습니다.
  • 다음날 뭘 할지 알게되니 미리 준비해올 수 있었어요(UI/UX 개선을 위해 각자 좋은 아이디어 생각해오기 같은..)

단점은

  • 막상 개발에 집중하게 되는 시간이 오면 문서화가 소홀해지는 느낌?(딱히 단점이라고 하기는 뭐하지만..?)

대부분이 장점이었네요 ㅋㅋㅋ 장점 가득한 페어 노트였습니다. 다음 페어는 없지만 레벨 3가 되면 적극적으로 활용하려 할 것 같아요!!

이번주는 신나는 데일리 조 회식 주차

이번 주 금요일 MAKER JUN과 함께하는 준조 회식을 진행해보았습니다. ㅋㅋㅋ 제가 워낙 까부는 걸 좋아해서 장소 선정 TF를 자진해서 했다는... (크루들을 위해 쓰는 시간은 아깝지 않아!! 아마도..)

방이 광안리라는 횟집에 가자고 했더니 모두들 반겨주셨다 ㅋㅋㅋ .. 대부분 맛으로는 만족, 양이 좀 부족하다는 평이 있었지만.. 그래도 장소 잡은게 어디임!! 주변에 회사들이 많아서 예약못할 줄 알았는데 괜찮은 가게를 예약하고 가게되어 행복했음 !!

헤어스타일이 아름다운 두 크루 덕분에 더 더 더 재밌게 놀았다능!! ㅠㅠ 다음날 되도 자꾸만 아놀드 헤어스타일이 떠올라서 웃프다 😭 다른 사람들이 보면 변태로 오해할 것 같아.. (피식피식 혼자 웃고다녀서)

아놀드 조금은 어려운 사람인 줄 알았는데 편견이 사라진 것 같다..!! 굉장히 순박하고 재밌는 사람인 것 같다 좀 더 친해진 것 같다는..?

기술회고

setState 이해하기 - 동기

우리는 상태를 업데이트 할 때 useState를 호출하여 받아내는 setState로 상태를 업데이트 합니다. 너무나도 당연하게 받아들이는 이 사실에 저는 궁금증이 생기더라구요 ! 상태를 직접 변경하면 어떻게 되는거지?

다음의 간단한 코드를 봅시다!

function App() {
  let [state, setState] = useState({ a: 0 });
  return (
    <div className="App">
      <div>{state.a}</div>
      
      <button onClick={() => setState((prev) => ({ ...prev, a: prev.a + 1 }))}>
        상태를 setState로 변경하는 버튼
      </button>
      
      <button
        onClick={() => {
          state = {
            a: 10,
          };
        }}
      >
        상태를 직접 변경하는 버튼
      </button>
    </div>
  );
}

상태를 직접 변경하는 버튼을 눌러봅시다 !!

아무런 변화도 생기지 않았습니다. 그럼 상태를 setState로 변경하는 버튼을 클릭해볼까요?

우리가 기대했던 바와 같이 상태가 변하여 UI도 변경된 것을 확인할 수 있습니다. 하지만 여기서 기대했던 동작은 상태를 직접 변경하는 버튼을 클릭하여 state의 값을 변경시켰으니 상태를 setState로 변경하는 버튼을 클릭하면 직접 변경된 state값을 기준으로 새로운 상태를 만들어내지 않을까 였습니다.

자자 말이 길었습니다~ 정리한번 하고 갑시다잉 👻👻👻 내가 지금까지 수행한 flow는 다음과 같습니다.

  1. 상태를 직접 변경하는 버튼을 클릭했다

  2. state 식별자가 가리키는 객체는 {a:10}이 된다.

  3. 상태를 setState로 변경하는 버튼을 클릭한다.

  4. 이전 state 값을 기준으로 상태를 업데이트 하도록 코드를 작성하였으니 {a:11}이 되어야한다.

  5. 하지만 state의 값은 {a:1}이다

이 과정을 정리하면

  • 직접 상태를 변경시키면 UI 변경을 트리거하지 않는다.

  • setState로 상태를 변경시키면 UI 변경을 트리거한다.

  • setState에 predicate를 넣어 이전 상태를 받아 상태를 업데이트 할 때의 이전 상태는 직접 변경한 상태의 값이 아니었다.

setState 이해하기 - 가설

내가 탐구하고 싶은 아이디어는 직접 변경하였을땐 UI 변경을 트리거하지 못하지만 setState로 변경하면 UI 변경을 트리거한다.setState가 생각하는 이전 상태값의 기준이다.

가설 1 : setState 함수 내부의 동작을 보면 상태를 업데이트 시키고 UI 변경도 트리거하는 어떠한 코드가 작성되어 있겠군

가설 2 : setState가 기억하는 이전 상태값은 단순히 useState로 받아냈던 state 식별자가 기억하는 값이 아니다!

setState 이해하기 - 소스 바라보기

가설 1 : setState 함수 내부의 동작을 보면 상태를 업데이트 시키고 UI 변경도 트리거하는 어떠한 코드가 작성되어 있겠군

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };

  hook.queue = queue;

  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));

// 우리에게 전달되는 배열은 아래 배열
  return [hook.memoizedState, dispatch];
}

useState가 처음 호출되면 위 mountRedcuer가 호출된다. 여기서 반환되는 배열이 결국 [state,setState] 인 것인데 state -> hook.memoizedState dispatch -> DispatchReducerAction 과 매핑되는 것을 알 수 있다.

결국 setState에 새로운 상태 혹은 새로운 상태를 만드는 콜백함수를 넣어 호출하는 행위는 이 DispatchReducerAction을 실행시키는 행위라고 할 수 있다. 그럼 가설을 증명하기 위해 DispatchReducerAction에서 렌더링 액션을 호출하는 지를 확인해봐야 겠다.

function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  if (__DEV__) {
    if (typeof arguments[3] === 'function') {
      console.error(
        "State updates from the useState() and useReducer() Hooks don't support the " +
          'second callback argument. To execute a side effect after ' +
          'rendering, declare it in the component body with useEffect().',
      );
    }
  }

  const lane = requestUpdateLane(fiber);

  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    // 큐에 업데이트를 푸시하는 함수겠지? 렌더페이스가 도는 중이라면 렌더페이스업데이트 작업을 큐
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    // 렌더페이스가 아니라면 업데이트 작업을 큐
    enqueueUpdate(fiber, queue, update, lane);

    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // 렌더 페이스에 들어가기전에 다음 상태를 계산한다.
      // 이전 상태와 같다면 탈출
   
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        if (__DEV__) {
          prevDispatcher = ReactCurrentDispatcher.current;
          ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          // 상태와 리듀서를 숨긴다. 
          // 렌더링 단계에 들어갈 때 까지 리듀서가 변경되지 않은 경우 리듀서를 다시 호출하지 않고 상태를 사용한다?
         
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // 스케쥴 하지 않는다 -> 리렌더 되지 않는다?
            
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactCurrentDispatcher.current = prevDispatcher;
          }
        }
      }
    }
    
    // 실제 업데이트를 스케쥴하는 코드가 아닐까?
    const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
    if (root !== null) {
      entangleTransitionUpdate(root, queue, lane);
    }
  }

  markUpdateInDevTools(fiber, lane, action);
}

굉장히 복잡한 코드지만 내가 보고싶은건 Render를 트리거하는 어떤 함수를 호출하는지에 대한 여부이다. 정확히 파악하긴 힘들지만 함수 네이밍과 리액트 개발자들이 달아놓은 주석으로 유추할 수 있는 동작들이 있다.

  • 렌더 페이스에 스케쥴한다.

  • 이전 상태와 같다면 스케쥴하지않는다.

결국 정리하자면 setState는 상태를 업데이트 할 뿐만 아니라 리렌더 작업을 관리하는구나 !!(렌더 페이스를 스케쥴하는 등의 동작으로 보았을 때) 그렇기 때문에 직접 변경하면 리렌더링이 되지 않는거구나 ! (내가 코드로 직접 Dispatch가 하는 작업을 작성하지 않는 이상!)

코드가 너무 어려워서 정확한 분석이 되었다고는 할 수 없지만 함수 네이밍과 주석으로 밖에 판단하지 못했다. 나도 언젠간 저 코드들을 분석할 수 있겠지?

React 톺아보기 요 글이 도움은 되었지만 ㅠㅠ 2년전 글이라 지금 리액트 코드와는 다른 점이 있어 참고만하였습니다. 실제 소스 코드

가설 2 : setState가 기억하는 이전 상태값은 단순히 useState로 받아냈던 state 식별자가 기억하는 값이 아니다!

setState((prev) => ({ ...prev, a: prev.a + 1 })

위와 같이 호출할 때 prev는 무엇을 기억하고 있을까?

mountReducer 반환 부를 보면 hook.memoizedState을 반환하고 있다. 결국 상태를 직접 변경한다는 것은 hook.memoizedState를 바꾼다는 것인데 setState의 콜백 인자를 확인해보면 직접 변경한 상태값이 아닌 어떤 값을 이전 값으로 기억한다.

이전에 보았던 dispatchSetState 함수 내부의 코드를 다시 한번 살펴보면 컴포넌트에 적용된 상태값을 이전 상태값으로 기억하고 동작하는 것을 알 수 있다.

  
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
){
  
 // ...
  
try {
  // hook.memoizedState가 아닌 queue.lastRenderedState를 현재 상태값으로 참조한다.
          const currentState: S = (queue.lastRenderedState: any);
  // 리듀서에 돌려 값을 얻어낼 때에도 컴포넌트에 적용된 마지막 상태를 현재 상태로 주입.
          const eagerState = lastRenderedReducer(currentState, action);
          
         
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
 		
            
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactCurrentDispatcher.current = prevDispatcher;
          }
        }
}

내가 코드로 상태를 직접 변경하더라도 setState를 사용할 때 기억하는 이전 상태값에 영향을 주지는 않는다는 것!!

// state를 직접 변경한 이후
// 아래와 같은 코드 형태라면 개발의도와는 다른 동작을 하게되겠지만 ㅋㅋ..
setState(state+1)

결론

  • 상태를 직접 변경하더라도 UI에 영향을 주지는 않는다. 하위 컴포넌트에 상태를 인자로 넘기고 있다하더라도.. 영향이 없다.. (리렌더를 트리거하지 않는다)

  • setState가 기억하는 이전 상태값은 컴포넌트에 적용된 마지막 상태값이다. 직접 변경하더라도 setState가 기억하는 이전 상태값이 되지는 않는다.

  • setStatepredicate를 주입하여 업데이트하는 경우가 아니라면 상태를 직접 변경 시 원하지 않는 동작이 수행될 수 있다.

Ref

profile
Frontend Ninja
post-custom-banner

3개의 댓글

comment-user-thumbnail
2022년 5월 16일

닌자시네요

1개의 답글
comment-user-thumbnail
2022년 5월 25일

닌자시네요

답글 달기