노코드 툴 만드는 사람들 – 자바스크립트 오브젝트의 무결성 보장하기

노코드 툴 만드는 사람들 – 자바스크립트 오브젝트의 무결성 보장하기

배경

나두아이오의 데이터 모델, NADOO DOM(이하 _DOM)

이번 글은 내부 시스템에 대해 자세히 설명하는 내용은 아니지만 주제와 관련있는 내부 DOM, NADOO DOM의 컨텍스트에 대해 먼저 간략히 공유하려고 한다. 실제로는 더 복잡한 구조를 가지지만 극도로 추상화해보았다.

로우코드에는 최소한의 코드가 개입하고, 노코드는 코드를 요구하지 않는다. 그렇다면 코드 없이 어떻게 개발하는 걸까? 이는 각 Low-Code/No-Code(LCNC) 솔루션의 철학에 따라 다르다. 일반적으로는 GUI 편집을 위한 에디터, 그리고 모델과 변수 시스템을 가진다.

우리의 솔루션 또한 비슷하다. 별도의 데이터 모델, NADOO DOM(이하 _DOM, 브라우저의 DOM과 구분하기 위함이다)과 Variable System을 기반으로 한다.

런타임 시점의 _DOM = Read-only

이제 나의 엉성한 그림을 _DOM에 대한 읽기/쓰기권한이 나타나는 구조로 변환해보았다. 엉성한 User-side도 추가되었다. Nadoo.io로 배포한 앱의 사용자를 의미한다.

_DOM에 대한 쓰기 권한은 에디터, 즉 편집모드에서만 유효하다. 앱이 실행되는 Runtime 시점에는 VariableSystem을 기반으로 파싱된 _DOM을 읽기 권한으로만 접근할 수 있는 단방향 플로우다.

예를들어 사용자가 TextField(input) 컴포넌트에 글자를 입력하면 사용자의 인터렉션은 _DOM에 연결된 콜백함수를 호출해 Variable System을 통해 변수를 업데이트한다. 그리고 업데이트된 변수 기반으로 재해석(resolve)된 _DOM이 출력된다.

결론적으로 Runtime 환경에서 원본 _DOM은 변경되지 않는다

그런데 어느날, 변조된 _DOM을 관찰하게 된다.

트러블슈팅

어디선가 오브젝트를 변조하고 있다

_DOM을 업데이트할 만한 함수를 추적해봤는데 전부 호출되지 않는다. 그럼에도 어디선가 _DOM을 변경한다. 당연히 의심가는 함수 중에서 발생한 오류일 것이라는 전제를 깨지 못해 디버깅에 시간이 걸렸다. 다시 처음부터 오브젝트가 변경된다면 setter가 호출될 것이다라는 가설에서 출발했고, 바로 찾아낼 수 있었다.

아래의 디버깅 함수로 오브젝트에 프록시를 걸어 setter가 호출되는 지점의 콜스택을 확인했다.

function createDeepProxy(obj) {
  const handler = {
    get(target, prop) {
      const value = target[prop];
      if (typeof value === "object" && value !== null) {
        return new Proxy(value, handler);
      }
      return value;
    },
    set(target, prop, value) {
      console.log(`Property ${prop} modified:`);
      console.log("New value:", JSON.stringify(value, null, 2));
      console.trace();
      debugger;
      target[prop] = value;
      return true;
    },
  };

  return new Proxy(obj, handler);
}

export default function App() {
  const person = {
    name: "김철수",
    age: 30,
    address: {
      city: "서울",
      country: "대한민국",
    },
  };

  const proxiedPerson = createDeepProxy(person);

  function modifyPerson() {
    proxiedPerson.name = "김영희";
    proxiedPerson.address.city = "부산";
  }

  function handleClick() {
    modifyPerson();
    console.log(proxiedPerson);
  }

  return (
    <div className="App">
      <button onClick={handleClick}>Modify Person</button>
    </div>
  );
}

원본 오브젝트에 연결된 참조값

다시 사용자가 TextFIeld 컴포넌트에 ‘hi!’라고 입력한 시나리오로 돌아가보자. Variable System 내부에서는 **_DOM**을 기반으로 변수를 파싱하고, 이를 실제 값으로 업데이트한다.

<변수 파싱 및 resolve 결과>

{ TextField.value: ‘hi!’ }

이번에는 위의 변수를 다른 컴포넌트에서 참조한 경우를 살펴보자. 이 컴포넌트는 중첩된 변수 구조를 가진다. 이 경우 내부 메커니즘에 따라 여러 단계의 해석(resolve)을 거친다.

<변수 파싱 결과>

{
	component.query: {
		rules: [{ value: $TextField.value}] // TextField.value를 변수로 참조
	},
	component.query.rules[0].value: $TextField.value, // TextField.value를 변수로 참조
}

<1차 resolve 결과>

{
	component.query: {
		rules: [{ value: $textField.value }]
	},
	component.query.rules[0].value: ‘hi’, // 먼저 resolve됨
}

<2차 resolve 결과>

{
	component.query: {
		rules: [{ value: ‘hi’ }] // component.query.rules[0].value값이 들어감
	},
	component.query.rules[0].value: ‘hi’,
}
  • 각 value는 원본 _DOM에서 파싱된 결과다. 즉 별도의 깊은 복사* 연산이 없었다면 원본 _DOM에 대한 참조값이 유지된다.
  • component.query를 key값으로 가지는 변수의 value는 오브젝트다. Resolve 과정에서 오브젝트의 일부가 component.query.rules[0].value값으로 대체된다.
  • 이 과정에서 원본 _DOM도 변형된다.

*자바스크립트의 객체 복사

  • 얕은 복사: 최상위 레벨에 대해서만 다른 참조값을 가진다. 즉 중첩된 구조의 경우 참조값이 연결되어 있다.
    • 방법: Object.assign(), 스프레드 연산자(…), Array.slice()
  • 깊은 복사: 모든 레벨의 프로퍼티에 대해 다른 참조값이 생성된다. 복사된 대상을 변경해도 원본 오브젝트에 영향을 미치지 않는다.
    • 방법: JSON.parse(JSON.stringify()), 재귀 함수를 사용한 수동 복사, Lodash 라이브러리의 _.cloneDeep() 메서드

근데 왜 지금까지 직접적인 문제 현상이 나타나지 않았을까?

이전에는 페이지를 전환할 때 전체 _DOM이 다시 로드되어 페이지네이션된 부분을 보여주었다. 그러나 성능 문제로 인해 클라이언트 사이드 라우팅 적용을 고려하는 과정에서, 이전에 방문했던 페이지로 돌아갔을 때 변조된 _DOM으로 인해 이전과 다르게 동작하는 현상을 관찰하게 된 것이다.

해결방안

개발자의 실수로인한 오브젝트 변형 방지하기, 오브젝트 동결

원본 _DOM으로부터 객체를 추출할 때마다 매번 깊은 복사 연산을 수행하는 것은 비용이 높다. 그리고 여전히 문제 상황과 같은 실수가 발생할 수 있다.

그래서 내가 제시한 솔루션은 오브젝트를 동결시키는 것이다. 오브젝트를 동결시킬 수 있는 방법 중에서도 Object.freeze는 각 속성을 readonly로 만든다.

const obj = { a: 100 };

console.log(Object.getOwnPropertyDescriptors(obj));
/* {
  a: {
    configurable: true,
    enumerable: true,
    value: 100,
    writable: true
  }
}
*/

Object.freeze(obj);

console.log(Object.getOwnPropertyDescriptors(obj));
/* {
  a: {
    configurable: false,
    enumerable: true,
    value: 100,
    writable: false
  }
}
*/

원본 오브젝트가 수정되면 strict모드 여부에 따라 타입에러를 뱉거나 조용히 무시된다. 그리고 중첩된 구조라면 재귀적으로 변경해야 한다.

// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#deep_freezing>
  function deepFreeze(object) {
    const propNames = Reflect.ownKeys(object);

    for (const name of propNames) {
      const value = object[name];

      if ((value && typeof value === "object") || typeof value === "function") {
        deepFreeze(value);
      }
    }

    return Object.freeze(object);
  }

  const obj2 = {
    internal: {
      a: null,
    },
  };

  deepFreeze(obj2);

  obj2.internal.a = "anotherValue"; // fails silently in non-strict mode
  (() => {
    "use strict";
    obj2.internal.a = "anotherValue"; // Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>'
  })();

결론적으로 위의 deepFreeze 함수로 _DOM을 감싸면 _DOM으로 부터 일부를 파싱하고, 재해석하는 과정에서 의도치않게 원본 _DOM까지 변경되는 것을 막을 수 있다.


이번 작업을 통해 에디터의 안정성에 기여할 수 있었다. 구조적 관점에서의 이슈를 발견할 수 있었던 새로운 경험이기도 했다.

뿐만 아니라 안정성 측면에서 보완이 필요한 작업들을 리스트업하는 데 계기가 되기도 하였다. 다음 편에서는 이와 관련해 리서치하고 적용해본 내용들을 공유하려고 한다.


솔루션의 비즈니스 가치를 고민하며 성장하는 개발자 김지후입니다.

—김지후, SW엔지니어 @ 나두모두

Read more

해외 바이어를 위한 인보이스 생성 및 발송 관리 서비스

해외 바이어를 위한 인보이스 생성 및 발송 관리 서비스

수출 업무를 하시는 분이라면 인보이스 관리가 얼마나 중요한지 아실 거예요. 한국에서는 홈택스와 연동된 전자세금계산서를 발행하면 모든 게 전자화되어 있어서 관리가 문제 없는데, 해외 거래처와는 인보이스가 중요한 문서이기 때문에 잘 관리를 해야 합니다. 우리 나라에서는 인보이스 (청구서)를 자동으로 생성하고 발송해주는 서비스가 없어서 많이들 엑셀을 사용하시는데요, 엑셀로 인보이스를 만들면 이력 및

By 나두아이오
AI로 완성하는 다국어 홈페이지!

AI로 완성하는 다국어 홈페이지!

해외 고객을 상대하는 기업에게 다국어 홈페이지는 필수입니다. 하지만 실제로 다국어 사이트를 만들고 운영하는 과정은 쉽지 않아요 ㅠㅠ 번역 비용, 개발 비용, 유지 관리 부담까지 고려하면 중소기업이나 1인 사업자에게는 큰 장벽이 되기 때문입니다. 나두아이오는 이런 문제를 해결하기 위해 클릭 몇 번으로 완성되는 AI 다국어 홈페이지 기능을 출시했어요! 1. 기능 활성화만으로 다국어

By 나두아이오
나두아이오 메이저 업데이트 소식!

나두아이오 메이저 업데이트 소식!

홈페이지를 통해 문의를 받으면 고객 관리, 문서 관리, 계약 관리 등 해야 할 일들이 생각보다 많으시다고 나두아이오 이용 고객님의 피드백을 종종 받곤 했어요. 계약서는 파일로 주고받고, 인보이스는 엑셀이나 다른 서비스에서 따로 만들고, 회사 도메인 이메일을 쓰지 못해 지메일이나 네이버 메일로 고객과 소통해야 하죠. 각각의 서비스를 왔다갔다 하면 업무를 처리하는 게

By 나두아이오
클릭 한 번으로 SEO, AEO, Sitemap까지 지원!

클릭 한 번으로 SEO, AEO, Sitemap까지 지원!

홈페이지를 멋지게 만드는 것도 중요하지만, 더 중요한 것은 잠재 고객들이 내 사이트를 '찾을 수 있게' 하는 것이에요. 아무리 잘 만든 홈페이지라도 검색 엔진에 노출되지 않으면 효과를 보기 어렵죠. 나두아이오에서는 검색 엔진 최적화를 복잡한 코딩 없이 클릭 한 번으로 SEO(검색 엔진 최적화) 및 AEO(AI 엔진 최적화)를

By 나두아이오