react rendering 과 state 변경감지

바다·2022년 11월 6일
0

react

목록 보기
4/10
post-thumbnail

notion 클론 코딩 프로젝트 중에 만난 오류를 해결하면서 공부하게 된 react rendering(렌더링), DOM 구현 순서와 state 관계를 정리해보려한다.

1.재렌더링

react에서 컴포넌트가 재랜더링 되는 때는 다음 중 하나의 조건을 만족할 대이다.

1) 내부 상태값(state) 변경

2) 부모가 전해준 속성(props) 변경

3)중앙 상태값(redux store 등) 변경

4) 부모 컴포넌트가 재렌더링되는 경우, 자식 컴포넌트도 재렌더링.

2. react 마운트,rendering시 dom 구현 순서

1) 함수 컴포넌트 추출

2) 구현부 실행

3) return 실행 (렌러딩 시작)

4) 렌더 단계 (Render Phase)
-재렌더링 과정에서는 이전의 가상 DOM과 비교하여 달라진 부분을 확인하여 실제 DOM에 반영할 부분을 결정
가상 DOM 생성

5) 커밋 단계(Commit Phase)
-실제 DOM에 반영
- 재렌러딩 과정에서는 달라는 부분만 실제 DOM에 반영

6) useLayoutEffect
-브라우저가 화면에 Paint 하기 전에, useLayoutEffect에 등록해둔 effect(부수효과함수)가 동기로 실행
-이때 state등 변경 사항이 있으면 한번 더 재렌러딩됨

7) Paint
-브라우저가 실제 DOM을 화면에 그림
- 마운트에서는 didMount 가 완료, 재런더링에서는 didUpdate가 완료
8) useEffect
화면에 DOM이 그려진 후 useEffect에 등록한 effect가 비동기로 실행

3. 오류를 해결하면서 배운 것들

1). root state 변경 시, useState와 const의 차이

만약 root state에서 변경된 property를 component에서 props로 받아 활용한다면 useState와 const를 사용하는 것은 별차이가 없을 것이다. 그러나 만약 다음과 같이 변경이 이루어지지않은 property로 다른 변경이 이루어진 property의 값을 구해야하는 상황이라면 어떨까? 이 경우에는 useState가 아닌 const을 사용하는게 더 좋은 방법이라고 생각한다..

  • 프로젝트에서 state와 component 구조

  • state의 type
type Block ={
  id:string,
  contents:string, 
  firstBlock:boolean,
  ....
} ;
type Page ={
  id:string , 
  header : {
		...
 }
  firstBlocksId :string[] | null,
  blocks : Block[] |null,  
  blocksId : string[] | null, 
    ....
};
type Notion={ 
  pagesId :string [] |null,
  firstPagesId: string[] |null,
  templatesId: string[]|null,
  pages: Page[] |null,
  trash:{
    pagesId:string[]|null,
    pages:TrashPage[]|null
  }
};
  • Frame.tsx
 const Frame =()=>{
   /** block의 id를 가지로 페이지에서 해당 block을 찾아 block을 반환하는 함수 
   * id: block.id
   *return block
   */
   const findBlock =(id:string):Block=>{.... return block};
   
   // 예시 코드1 
   const firstBlocksId:string[]|null =page.firstBlocksId;
   const firstBlocks:Block[]|null = firstBlocksId ===null? null: firsBlocksId.map((id:string)=> findBlock(id));
   .....
   
   
   return (
   ...
     {
       firstBlocks.map((block:Block)=>
        	<EditableBlock
        		block={block}
      			......
        	/>
     }
   
   )
 }
  • notin.ts에서 block data를 변경하는 방식

    splice 메소드를 사용하여 수정 될 block이 있는 page에서 수정 이전의 block과 수정된 block을 바꿔 치기하는 방법으로 state를 변경함

function notion (state:Notion =initialState , action :NotionAction) :Notion 
	....
  const pages =state.pages !==null?  [...state.pages] : null;
  ...
  case EDIT_BLOCK :
  	....
  	...
  const targetPage = 수정된 block이 있는 page;	
const blockIndex = targetPage.blocks에서 block의 index;
targetPage.splice(blockIndex,1,action.editedBlock);
// action.editedBlock : 수정된 data가 담긴 block 
..
return {
          pages:pages,
          firstPagesId:firstPagesId,
          templatesId:templatesId,
          pagesId:pagesId,
          trash:trash
        }
}

위와 같은 상황에서 block을 수정하면 root state가 변경되어, NotionRouter가 재렌러딩되고 그 아래에 있는 자식 요소들도 재렌더링된다.

그러나 아래 예시 코드2 처럼 useState를 이용한다면, firstBlocks는 재렌러딩되지 않아 변경사항이 dom에 재현되지 않았다.
page.firstBlockId는 변경사항이 없기 때문에 useState가 작동하지 않은 것 같다.

  • 예시 코드2.
    const [firstBlocks, setFrsitBlocks]=useState<Block[]|null>
          (page.firstBlocksId ==null
           ?
           	null 
           :
           	page.firsBlocksId.map((id:string)=>
                                     findBlock(id)));

2) useState의 문제점을 보완할 return문 활용

만약 useState를 사용하더라도 Frame.tsx에서 구현부가 아닌 return 문에서 map를 사용해 firtBlocks를 구하고 이를 활용한다면 변경된 block을 dom에 구현할 수 있다.
그 이유는 page.firstBlocksId의 변경이 없어서 useState를 이용한 firstBlocks의 값을 변경할 수 없지만, findBlock의 함수는 변경된 데이터를 반영한 page의 데이터를 활용해 block의 값을 반환하기 때문에 return 문에서 block의 값을 가져온다면 변경사항을 dom에 실현할 수 있게 된다.

  • return 문 활용한 data 변경 구현
    const Frame=()=>{
     ....
    
      return (
         
         ...
           {firstBlocksId.map((id:string)=>findBlock(id))
            .map((block:Block)=>
           	<EditableBlock
           		block={block}
         			......
           	/>
         		)
       	}
         )
    }

그러나 여기에도 오류는 존재했다.
Frame 컴포넌트를 자식 요소로 활용하는 Templates(template(type Page)을 보여주는 컴포넌트)에서 다른 template로 이동할 경우, return 문에서 firstBlocksId은 이동할 template이 아닌 이동 전 template의 firstBlocksId이 였고 findBlock은 이동할 template의 data를 활용해 block 값이 undefined인 오류가 발생했다.

3) useEffect : react의 구현순서를 이용한 2) 단점 보완

return문이 이전의 data가 아닌 유저가 사용하고자 하는 data를 활용해 발생하는 문제이기 때문에 return문이 유저가 사용하고자 하는 data를 사용할 준비가 되었는지를 판단하고 , 준비가 되었을 경우 코드를 실행할 수 있도록한다면 오류는 해결할 수 있다.

이때 활용할 수 있는데 useEffect이다. useEffect는 return문이 실행된 이후에 실행되기 때문에 다음과 같은 코드를 통해 오류는 해결할 수 있다.

 const Frame =()=>{
 	....
    const pageId =useRef<string>("");
   
   ...
   useEffect(()=>{
   	pageId.current=page.id;
   },[]);
   
   return(
   	{ pageId.current === page.id &&
      firstBlocksId.map((id:string)=>findBlock(id))
         .map((block:Block)=>
        	<EditableBlock
        		block={block}
      			......
        	/>
      		)
    	}
   )
 }

4.마무리

react의 state의 형태가 복잡해질 수록 state의 변경을 감지하는게 어려려워지는 것 같다. react의 root state를 활용할 때(state안의 props가 객체나 배열의 형태라면 더욱이) useState보다는 const가 더 적합한것 같다는 생각이 들었다. retrun문이나 useEffect로 보완이 가능하지만, const 를 쓰는 것이 발생하는 오류가 더 적었다.
비록 오류를 해결하는 과정에서 머리가 복잘했지만 react가 dom을 구현하는 순서,방식에 대해 공부해 보는 좋은 계기가 됐다.

참고자료

React 컴포넌트 렌더링 과정 정리(useLayoutEffect vs. useEffect)

profile
🐣프론트 개발 공부 중 (우테코 6기)

0개의 댓글