번역 라이브러리 개선


아래 예시와 같이, 번역파일의 특수문자 이스케이프 처리를 누락한 경우에 원본과 번역본의 형식 차이로 인해 병합 과정에서 에러가 발생했습니다.

messages.xlf
<!-- 원본 번역파일 (messages.xlf) -->
<source>Dog &amp; Cat</source>

<!-- 번역 파일 (messages.fr.xlf) -->
<source>Dog &amp; Cat</source>
<!-- 특수문자 이스케이프 처리 누락-->
<target>Chien & Chat</target>

구현

처음에는 문자열을 정규식으로 치환하여 누락된 이스케이프를 보정하는 방법을 고려했습니다.

하지만 XML은 태그와 속성, 텍스트가 하나의 문자열 안에 함께 존재하기 때문에 문자열 단위의 치환으로 처리할 경우 XML 구조를 훼손할 가능성이 있었습니다.

따라서 파일 전체를 문자열로 처리하는 대신, DOMParser를 통해 XML을 DOM 트리로 변환한 뒤 보정이 필요한 텍스트 및 속성 노드만 순회하는 방식으로 구현했습니다.

builder.ts
// Before
const translationSourceFile = await fs.readFile(sourcePath, 'utf8');              
const translationTargetFile = await fs.readFile(targetPath, 'utf8');

// After
const translationSourceXml = await fs.readFile(sourcePath, 'utf8');
const translationSourceDoc = new DOMParser().parseFromString(translationSourceXml, "text/html");
const translationSourceFile = escapeParser(translationSourceDoc);

const translationTargetXml = await fs.readFile(targetPath, 'utf8');
const translationTargetDoc = new DOMParser().parseFromString(translationTargetXml, "text/html");
const translationTargetFile = escapeParser(translationTargetDoc);
parser.ts
// TEXT_NODE, ATTRIBUTE_NODE에서 xmlEncoder 함수 호출
case node.ATTRIBUTE_NODE:
    const attrNode = node as Attr;
    return buf.push(' ', attrNode.name, '="', attrNode.value.replace(/[<&"]/g, _xmlEncoder), '"');
case node.TEXT_NODE:
    const textNode = node as Text;
    if (!options.beautify || partOfMixedContent || !containsOnlyWhiteSpace(textNode.data)) {
        return buf.push(textNode.data.replace(/[<&]/g, _xmlEncoder));
    }
    return;

// xmlEncoder
function _xmlEncoder(c: string): string {
  return c === '<' && '&lt;' ||
    c === '>' && '&gt;' ||
    c === '&' && '&amp;' ||
    c === '"' && '&quot;' ||
    '&#' + c.charCodeAt(0) + ';';
}

검증

원본과 번역본 모두 이스케이프 처리가 정상적으로 적용되는지 테스트 코드를 통해 검증했습니다.

builder.spec.ts
test('extract-and-merge xlf 2.0', async () => {
  // 원본 번역 파일
  await fs.writeFile(
    'builder-test/messages.xlf',
    `<xliff version="2.0">
        <file original="ng.template" id="ngi18n">
          <unit id="ID1">
            <segment>
              <source>source &amp; val</source>
            </segment>
          </unit>
        </file>
      </xliff>`,
    'utf8',
  );

  // 번역 파일
  await fs.writeFile(
    'builder-test/messages.fr.xlf',
    `<xliff version="2.0">
        <file original="ng.template" id="ngi18n">
          <unit id="ID1">
            <segment>
              <source>source &amp; val</source>
              <target>target & val</target> // 의도적으로 이스케이프 처리 누락
            </segment>
          </unit>
        </file>
      </xliff>`,
    'utf8',
  );

  const run = await architect.scheduleTarget(
    {
      project: 'builder-test',
      target: 'extract-i18n-merge',
    },
    {
      format: 'xlf2',
      targetFiles: ['messages.fr.xlf'],
      outputPath: 'builder-test',
    },
  );

  const result = await run.result;
  expect(result.success).toBeTruthy();
  await run.stop();

  // 이스케이프가 보정된 최종 결과 검증
  const targetContent = await fs.readFile(
    'builder-test/messages.fr.xlf',
    'utf8',
  );

  expect(targetContent).toContain(
    '<target>target &amp; val</target>',
  );
});

결과

번역 과정에서 발생할 수 있는 특수문자 이스케이프 누락을 빌드 단계에서 보정하도록 변경하여, 해당 문제로 발생하던 병합 오류를 방지했습니다.

Flutter Android Webview 프레임드랍 최적화


Flutter의 Android 렌더링 구조에 의해 발생한 문제였습니다.
Flutter는 자체 렌더링 엔진(Skia/Impeller)을 사용하는데, Webview 같은 네이티브 Android 뷰를 Flutter 위에 올릴 때 발생합니다.

기본 방식: Virtual Display (하드웨어 가속 활성화)
네트워크 요청 발생
→ Android WebView가 응답 처리
→ WebView 콘텐츠를 SurfaceTexture에 렌더링 (GPU)
→ Flutter 엔진이 SurfaceTexture를 자신의 렌더링 파이프라인에 합성
→ 이 합성 과정에서 GPU 렌더링 스레드와 메인 스레드가 동기화 필요
→ 네트워크 응답으로 WebView가 리페인트되면
→ SurfaceTexture 업데이트 → Flutter 합성 동기화 → 블로킹

WebView가 GPU로 그린 결과를 Flutter가 텍스처(SurfaceTexture)로 받아서 다시 합성하는 구조라, WebView가 업데이트될 때마다 두 렌더링 파이프라인이 동기화 포인트를 만듭니다.

네트워크 응답 이후 DOM이 업데이트되면 위 동기화 과정에서 메인 스레드를 블로킹하면서 프레임 드랍이 발생했습니다.

해결 방식

해결 방식1: 하드웨어 가속 비활성화
하드웨어 가속 비활성화
→ WebView가 SurfaceTexture(GPU) 대신 소프트웨어 렌더링(CPU)
→ Flutter와 GPU 리소스를 공유하지 않음
→ 동기화 오버헤드 제거
→ 프레임 드랍 없음

GPU 파이프라인에서 분리되니 충돌이 없어집니다. 다만 소프트웨어 렌더링(CPU)이라 복잡한 UI는 느릴 수 있었습니다.

해결 방식2: Hybrid Composition 활성화
Hybrid Composition 활성화
→ WebView를 SurfaceTexture가 아닌 Android View 계층에 직접 삽입
→ Flutter가 WebView 위/아래에 별도 레이어를 합성
→ WebView는 Android 자체 렌더링 파이프라인으로 독립 실행
→ 네트워크 응답 → WebView 업데이트가 Flutter 스레드와 무관
→ 동기화 블로킹 없음

WebView가 Flutter 렌더링 파이프라인 밖에서 독립적으로 그려지기 때문에 서로 간섭하지 않습니다.

결과

해결 방식2를 파트너사에 제안하여 채택 이후, 웹뷰에서 발생했던 프레임드랍 현상을 해소했습니다.