🎉 단 50만원으로 홈페이지 제작 서비스 이벤트! 👉 자세히 보기

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

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

배경

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

CS팀을 위한 AI 에이전트 활용법

CS팀을 위한 AI 에이전트 활용법

우리 회사의 방대한 매뉴얼, AI로 어떻게 대응하지? 고객지원팀은 종종 이렇게 말합니다. “답은 있는데, 찾기가 너무 어렵다.” 특히 보험업계처럼 약관, 상품군, 예외 조건이 복잡한 산업에서는 정보 접근성 문제가 고객 경험 전반을 좌우합니다. 수십 페이지에 달하는 내부 문서와 매뉴얼을 매번 찾고, 확인하고, 설명해야 하죠. 이것도 잘 몰라서 담당자에게 토스하고, 그 과정에서 고객은

By 나두아이오
우리 기업의 지식을 이해하고 활용하는 AI

우리 기업의 지식을 이해하고 활용하는 AI

챗GPT을 경험한 사람이라면 어떻게 AI기술을 내 서비스에, 내 업무에, 내 제품에 활용해서 혁신을 이룰까 고민해봤을 겁니다. 이미 많은 기업들이 생성형 AI를 도입을 시도하고 있고요. 그러나 기업이 가장 자주 마주하는 문제는 “범용적인 AI는 내 조직의 문맥을 모른다”는 점입니다. 단순히 똑똑한 AI보다 중요한 건, 우리 조직의 지식을 이해하고 활용하는 AI이니까요. 조직

By 나두아이오
이벤트 페이지 만들기 (feat. 자동차 시승 신청 페이지)

이벤트 페이지 만들기 (feat. 자동차 시승 신청 페이지)

나두아이오를 통해 근사한 채용 사이트 만들기를 배워보았는데요. 이번에는 디자인이 좀 들어간 간단한 이벤트 페이지 만드는 법을 배워보겠습니다. 저희 고객 사례인 신차 시승 이벤트를 예로 들어 설명드릴게요. 페이지 꾸미기 채용사이트 만들기 글에서는 노션 페이지를 그대로 가져오는 법을 배웠는데요, 여기서는 간단한 디자인 편집하는 방법을 알려드릴게요. 나두아이오에서 새 인터페이스를 생성하면 간단한 디자인이 있는데

By 나두아이오