JIGGAG

8월 한달동안 로그

2025년 9월 6일

Kotlin conf

  • 키노트 KMP/CMP
    • 앱도 이제 stable release 라고 한다
      • 여태 아니였다는게 놀랍기도
    • compose UI 가 너무 매력적으로 들린다
  • Compose로 만드는 창의적인 UI
    • 직사각형을 배열하는 것
    • 그리고 멋을 내고 싶다면 곡선을 추가한다
    • 여기서는 게임 UI를 compose ui 로 구현해내었는데
    • 진짜 영끌이다
    • 좌표 계산하고 애니메이션 효과를 줄때 느낄 수 있는 희열 🥳
  • Compose Hot Reload 구현하기
    • 핫리로드를 구현하면서 겪은 문제들
      • 사용자에게 제한을 주고 싶지 않다
      • 최대한 자연스럽게
      • 사용자는 모르게
    • 방금 변경된 코드가 어디서 사용중인지 역으로 따라가 핫리로드 구현하는 방식
      • 리액트 vdom 처럼 변경된 곳을 찾아 그 부분을 바꿔준다는 것이 비슷해보이는데
  • 실전 투입 가능한 iOS용 Compose Multiplatform
    • iOS API를 모두 kotlin 으로 그릴 수 있긴 하지만
    • 어느정도 네이티브 API 알고 있어야 수월하겠다
  • 여기까지 듣다보니 KMP/CMP 차이가 있나?
    • KMP 에서 kotlin으로 구성할 수 있던 안드로이드가 아닌
    • iOS나 웹, 데탑앱을 그리기 위해 CMP를 만들었고
    • 실제로는 KMP 프로젝트에서 CMP를 통해 각각의 네이티브 UI API를 호출하게 되는 모양
    • compose UI 를 멀티플랫폼에서 확장하여 사용하기 위한 것
    • 참고
      • Kotlin Multiplatform은 네이티브 프로그래밍의 이점을 유지하면서 다양한 플랫폼에 적합한 애플리케이션을 제작하고 여러 플랫폼에서 코드를 효율적으로 재사용할 수 있도록 해주는 기술입니다. 애플리케이션이 iOS, Android, macOS, Windows 및 Linux 등에서 실행됩니다.
      • JetBrains의 선언형 UI 프레임워크인 Compose Multiplatform을 활용하면 Android, iOS, 데스크톱과 웹에서 공유되는 UI를 개발할 수 있습니다. Compose Multiplatform을 Kotlin Multiplatform 프로젝트에 통합하여 UI 구현을 여러 개 유지할 필요 없이 앱과 기능을 더 빠르게 전달하세요.
  • Duolingo + KMP: 개발자 생산성에 대한 사례 연구
    • 듀오링고가 리액네, 플러터를 KMP로 전환했다
    • 플랫폼의 공통된 안정성을 높이는데 집중했다
      • 특정 OS에서만 문제가 발생한다는 등
    • 기능적인 것부터 포팅을 시작했고
    • 그리고 CMP에 큰 관심이 가고 넥스트 스텝으로 갈 예정이라고 한다
    • 각각 분리되어있을때보다 더 나은 생산성을 느낀다고 하는데
      • 처음 학습할때의 어려움은 있을테지만
      • 최종적으로는 점점 출시가 빨라짐
  • KMP를 처음 들었을때부터 관심이 있었다
    • KMM 이였는데 어느샌가 사라졌다 M 🤔
    • 당연히 멀티 플랫폼이라는 것이 휘둥그레지게하였다
    • 그리고 네이티브 언어를 익힐 수 있다는 것도 한 몫
    • 호기롭게 시작한 프로젝트는 바로 접었는데
    • 코틀린을 적당히 읽고 쓸 수 있었지만
      • 코틀린에 대해서만 스터디 했다
      • (그러고보니 스터디 하면서 사이드 앱을 만들었는데, 각자 코틀린으로 서버/앱을 만들다 책을 다 읽은 시점에 플젝도 마감 처리 했다는 슬픈 사실)
    • UI를 그리기 위해서는 결국 xml을 손대야만 했다
      • 이것은 엄청난 장벽이였다
      • 개발을 독학해보겠어 하던 시절에도 부딪혔던 벽
    • 비즈니스 로직이라도 공유할 수 있다는 장점이 있지만
      • UI가 큰 장벽인 나에겐 결국 네이티브 앱을 각각 만드는 것과 비슷했다
    • 지금은 AI가 도와줄 수 있으니
      • 그 당시보다 상황은 나았을지도 모르겠다

리액트 언마운트 vs 리렌더링

  • 🥊 key가 바뀌면 어떻게 되나
    • 언마운트 vs 리렌더링
    • key 가 바뀌면 이전 컴포넌트는 언마운트 후 새로운 컴포넌트 마운트
      • 언마운트: DOM에서 제거
      • 마운트: DOM에 추가
    <User key={userId}>
      <Common />
    </User>
    
    • userId 라는 key 가 바뀌면 User 라는 컴포넌트 자체가 DOM에서 사라졌다가 다시 추가 되는 것이기에
      • 그 아래에 있는 Common 도 아예 새로 추가 되는 것
    • 리렌더링 되는 것이 아니기에 아래 Common이 유지될 수 없음
      • 따라서 상태나 애니메이션이 초기화 됨
    • key가 동일하면 동일한 DOM을 유지하기 때문에
      • 다른 props는 변경되어도 리렌더링 되는데
      • key는 언마운트 처리 되는 차이가 있다
    • 리액트 재조정 과정에서 key가 변경되었는지를 우선 확인하여 트리 구조가 변경되었는지 판단하기 때문에 고유한 key를 사용하도록 하여 퍼포먼스 이점을 가지도록 한다
  • 🥊 diffing과 reconciliation
    • https://roseline.oopy.io/dev/react-advanced-deep-dive-into-diffing-and-reconciliation 내용을 따라가며 학습한다
    • 조건부 렌더링 하는 경우
      {
        isCompany ? (
          <Input id="company-tax-id" placeholder="Enter your company Tax ID" onChange={handleCompanyChange} />
        ) : (
          <Input id="personal-tax-id" placeholder="Enter your personal Tax ID" onChange={handlePersonalChange} />
        );
      }
      
      • 조건에 따라 props가 모두 바뀌었고 아예 다른 동작을 해야하도록 구현했다
      • 하지만 isCompany라는 상태에 따라 새로 그려지리라 기대했던 텍스트 인풋에 입력된 내용은 사라지지 않고 유지되었다
        • 새로운 컴포넌트로 마운트 되었다면 인풋도 초기화 되었어야하는데 말이다
    • 리액트가 컴포넌트가 바뀌었다고 판단하는 조건
      • type이나 key가 바뀌었을때
      • 위 예시에서는 type이 Input으로 동일하며 key가 존재하지 않기 때문에
      • 동일한 컴포넌트의 props만 바뀌어서 리렌더링 되었다
        • 따라서 인풋에 입력했던 내용은 그대로 유지되었다
      • reconciliation에 의한 것으로
        • type을 바꿀 수 없다면 위 예시에서는 key를 다르게 주어 새로 마운트 하도록 해야한다
    • Virtual DOM 트리 비교
      const Component = () => {
        const [isCompany, setIsCompany] = useState(false);
      
        return (
          <div>
            <Input id="id1" placeholder="placeholder1" />
            {isCompany ? <Input id="id2" placeholder="placeholder2" /> : <TextPlaceholder />}
          </div>
        );
      };
      
    • Component 컴포넌트를 Virtual DOM 트리로 바꾸면
      • 사용자 컴포넌트는 type에 함수 그대로 참조된다
      {
        type: 'div',
        props: {
          children: [
            {
              type: Input,
              props: { id: "id1", placeholder: "placeholder1" }
            },
            {
              type: TextPlaceholder,
            },
          ]
        }
      }
      
    • 이러한 형태인데 state가 바뀌면 Component 자체가 다시 렌더링을 시작하며 변경된 Virtual DOM 트리와 비교 과정을 통한다
      {
        type: 'div',
        props: {
          children: [
            {
              type: Input,
              props: { id: "id1", placeholder: "placeholder1" }
            },
            {
              type: Input,
              props: { id: "id2", placeholder: "placeholder2" }
            },
          ]
        }
      }
      
      • 첫번째 type인 div 는 변하지 않았으므로 패스
      • 그 아래 children의 첫번째 type도 Input 그대로 동일하므로 패스
      • 하지만 두번째 type 이 TextPlaceholder에서 Input으로 바뀌었으므로 새로 마운트 하며
      • children 자체도 바뀐게 되므로 div도 리렌더 된다
    • 이런식으로 Component가 상태 변경에 의해 리렌더링 되는데
      • 그 안에 인라인으로 작성한 경우
        const TextPlaceholder = () => {};
        
        const Component = () => {
          const [isCompany, setIsCompany] = useState(false);
          const Input = () => {};
        
          return (
            <div>
              <Input id="id1" placeholder="placeholder1" />
              {isCompany ? <Input id="id2" placeholder="placeholder2" /> : <TextPlaceholder />}
            </div>
          );
        };
        
        • 트리 type 에서 참조하는 Input 은 리렌더링마다 새로운 함수로 반환되므로
        • 항상 리액트 type 비교에 실패하므로 항상 새로 마운트 된다 🚨
          • 성능에 무척 치명적이다
    • key가 동일한데 위치가 바뀐 경우엔 어떻게 될까?
      <>
        <Checkbox />
        {isCompany ? <Input key="company" id="company-tax-id" /> : null}
        {!isCompany ? <Input key="person" id="person-tax-id1" /> : null}
        {isCompany ? <Input key="person" id="person-tax-id2" /> : null}
      </>
      
      // isCompany = true
      [
        { type: Checkbox },
        { type: Input, props: { id: 'company-tax-id' } },
        null,
        { type: Input, props: { id: 'person-tax-id2' } }, // key=person
      ][
        // isCompany = false
        ({ type: Checkbox },
        null,
        { type: Input, props: { id: 'person-tax-id1' } }, // key=person 언마운트 후 마운트
        null)
      ];
      
      • 상태 변경에 따라 Checkbox의 type은 동일하기에 컴포넌트 유지된다
      • key가 company인 Input 컴포넌트는 언마운트 되었다
        • null이 되어 사라졌다
      • key가 person인 Input 컴포넌트는 새로 마운트 되었다
        • DOM에서 위치만 바뀌었을뿐 key가 같아 동일한 컴포넌트로 유지되지 않을까 싶지만
        • 배열이 아닌 일반 요소에서는 유지 되지 않고 새로 마운트 된다
    • 만약 배열이였다면
      // data가 [1,2,3] 에서 [2,1,3] 으로 바뀌었을때
      <>
      	{data.map(item => <Input key={item} value={item} />}
      	<Input key="1" value={111} />
      </>
      
      // data = [1,2,3]
      {
        [
          { type: Input, props: { value: 1 } },
          { type: Input, props: { value: 2 } },
          { type: Input, props: { value: 3 } },
        ], // 배열 요소
          { type: Input, props: { value: 111 } }; // 일반 요소
      }
      
      // data = [2,1,3]
      {
        [
          { type: Input, props: { value: 2 } }, // 리렌더링
          { type: Input, props: { value: 1 } }, // 리렌더링
          { type: Input, props: { value: 3 } }, // 리렌더링 되지 않음
        ],
          { type: Input, props: { value: 111 } }; // 리렌더링 되지 않음
      }
      
      • 배열과 일반 요소의 트리는 아예 분리되어있으며
      • 같은 배열 내에서 동일한 key의 컴포넌트가 위치만 바뀌었으므로
        • Input 컴포넌트 언마운트 되지 않고 리렌더링 된다
  • 🥊 key와 type을 다시 정리해보면
    • key가 같은 경우
      • 위치와 타입이 같은 경우 > 리렌더링
      • 타입이 다른 경우 > 언마운트 후 마운트
        • 이때 위치는 같던 다르던 상관없다
      • 위치가 다르지만 타입이 같은 경우
        • 배열 요소 > 리렌더
          • 다른 위치로 이동했다고 판단
        • 일반 요소 > 언마운트 후 마운트
          • 위치가 달라지면 아예 다른 컴포넌트로 판단
    • key가 다른 경우
      • 위치, 타입 모두 상관 없이 > 언마운트 후 마운트