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

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

배경

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

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

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

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

By 나두아이오
홈페이지+폼+데이터베이스. 나두아이오의 올인원 서비스

홈페이지+폼+데이터베이스. 나두아이오의 올인원 서비스

홈페이지를 개설하고 운영하려면 보통 세 가지 도구가 필요한데요. 바로 홈페이지, 폼 서비스, 데이터 저장소 (데이터베이스)입니다. 이 3개의 서비스를 다 따로 사용하시는 분들이 계신데 시간이 지날수록 데이터가 여러 곳에 흩어지고 관리가 복잡해지는 경향이 있어요. 나두아이오는 이러한 불편함을 해결하기 위해 홈페이지·폼·데이터베이스가 한 곳에서 자동으로 연동되는 올인원 인프라를 제공합니다. 홈페이지·

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

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

✨ 새로운 기능 🤖 AI 이미지 생성 이제 텍스트 설명만으로 원하는 이미지를 만들 수 있습니다. AI가 당신의 아이디어를 멋진 이미지로 바꿔드립니다: * 텍스트로 설명하면 AI가 이미지를 생성해요 * 기존 이미지를 AI가 더 나은 품질로 개선해줘요 * 생성된 이미지를 바로 디자인에 적용할 수 있어요 * 무료 사용자는 하루 5회, 유료 사용자는 베타 기간 동안 무제한 무료로 사용할

By 나두아이오
OpenAI Founder Day에 초대받았어요!

OpenAI Founder Day에 초대받았어요!

나두모두가 이번 OpenAI Founder Day에 초대받아 다녀왔습니다! AI 혁명의 시초인 OpenAI가 이번에 한국 지사를 설립하면서 개최된 행사이에요. Founder Day에는 OpenAI가 선발한 창업자와 스타트업들이 초청되어, OpenAI 리더십과 직접 교류하며 최신 기술 방향성과 협력 기회를 논의하는 행사였습니다. 오직 100명의 창업자들만 초대받는 제한된 규모의 행사라 더욱 의미가 컸는데요, 나두아이오와 나두AI로 열심히 달려온 보람을

By 나두아이오