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

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

배경

나두아이오의 데이터 모델, 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

'모두의 창업' 공식 파트너로 나두아이오가 함께합니다!

'모두의 창업' 공식 파트너로 나두아이오가 함께합니다!

올해 스타트업 씬에서 가장 뜨거운 관심을 받고 있는 '모두의 창업'프로젝트, 다들 계시죠? 오픈 단 12일 만에 1만 개 이상의 프로젝트가 지원하며 그 인기가 대단했습니다. 엄격한 사업성 및 제품 평가를 거쳐 선정된 최종 팀에게는 10억 원 이상의 지원금과 투자 연계라는 파격적인 혜택이 주어지는데요. 창업가들의 꿈을 실현할 수 있는

By 나두아이오
업데이트 — 가장 오래 걸리던 작업들이 가벼워졌어요

업데이트 — 가장 오래 걸리던 작업들이 가벼워졌어요

새 페이지를 만들고, 콘텐츠를 쌓고, 디자인을 다듬는 일 — 가장 시간이 많이 들었던 작업들을 훨씬 짧고 가볍게 끝낼 수 있게 됐어요. 이번 업데이트의 핵심 변화들을 만나보세요. ✨ AI 페이지 생성 새 페이지를 만들 때 가장 막막했던 두 가지 — "어떤 섹션을 넣어야 하지?", "글은 또 어떻게 쓰지?" — 이걸 AI가

By 나두아이오
나두아이오 vs 워드프레스: 복잡한 관리에서 벗어나 비즈니스에 집중하는 방법

나두아이오 vs 워드프레스: 복잡한 관리에서 벗어나 비즈니스에 집중하는 방법

홈페이지를 구축할 때 가장 먼저 떠올리는 이름은 단연 워드프레스일 것입니다. 하지만 자유도가 높다는 것은 그만큼 관리해야 할 영역이 넓다는 뜻이기도 합니다. 오늘은 전통적인 강자 워드프레스와, AI 기술로 비즈니스 운영을 단순화하는 나두아이오를 비교해 보겠습니다. 한눈에 보는 핵심 비교 구분나두아이오WordPress (워드프레스)사이트 구축AI가 30초 만에 전체 구조 완성테마 선택, 플러그인 설치 등

By 나두아이오
나두아이오 vs Squarespace: 사업자를 위한 최적의 비즈니스 플랫폼 선택하기

나두아이오 vs Squarespace: 사업자를 위한 최적의 비즈니스 플랫폼 선택하기

웹사이트 빌더 시장은 크게 두 가지 흐름으로 나뉩니다. 시각적인 아름다움을 극대화한 Squarespace와, AI를 통해 비즈니스 운영의 모든 과정을 통합하려는 Nadoo가 그 중심에 있습니다. 오늘은 이 두 서비스가 사업 운영 측면에서 어떤 차이를 보이는지 상세히 비교해 보겠습니다. 핵심 비교 요약 구분나두아이오Squarespace (스퀘어스페이스)사이트 구축 방식AI 기반 자동 생성 (약 30초)템플릿

By 나두아이오