노코드 툴?
노코드 툴이라고 하면 환영하지 않는 개발자들이 대부분일 것이라고 생각한다. 코드 없이 웹 페이지를 만들 수 있음은 결국에 개발자들의 일자리 문제와 직결되기 때문이다. 개발자가 AI로 대체되느냐 마냐 하는 마당에, ‘노코드 툴’의 존재는 개발자들을 더욱 예민하게 만든다.
개발자들에게 노코드 툴을 사용하게 만들면 반발은 더욱 커진다. 키보드 두드리면 금방 만드는 웹 페이지를 굳이 이 툴을 새롭게 배워가면서 시간을 더 들여서 만들어야 된다고? 내가 어떻게 공부한 기술인데 그걸 쓰지 말라고?
코더가 되지 말고 개발자가 되어라
라는 말을 들어보았을 것이다. 이 말에 고개를 끄덕이지 않는 개발자는 거의 없을 거라고 생각한다.
축구용품을 판매하는 커머스를 만든다고 생각해보자. 브랜딩도 하고, 디자인도 하고, AI도 붙이고, 사용자들을 끌어들일 만한 컨텐츠들을 개발해서 멋진 홈페이지를 만들었고, 힘들기도 했지만 그 과정이 너무나 재미있고 뿌듯했다.
이제 물건만 팔면 되는데, 누가 얼만큼 주문을 했고, 재고는 얼만큼 남았는지, 홈페이지에 어떤 제품을 먼저 노출시킬 건지, 관리할 수 있는 어드민 페이지가 없다.
크게 생각할 필요도 없고 데이터베이스를 연결하고 몇 개의 페이지를 만들어서 올리면 되는데, 바빠 죽겠는 마당에 이런 단순 작업에 쓰는 시간이 너무 아깝다.
서비스는 고정된 개념이 아니기에 이러한 사이클은 서비스가 유지되는 동안 주기적으로 반복되며 개발자들을 고통받게 한다. 우리는 반복되는 어드민 개발에서 해방되고 싶다. 나에게 스트레스를 주는 건 마찬가지지만, 내 손으로 해결하고 싶고, 열정을 쏟고 몰입하게 만드는 더 크고 복잡한 문제를 엔지니어링하고 싶다.
앞으로 나두모두에서 이러한 니즈를 풀어나가는 과정을 하나하나씩 블로그에 담아보려고 한다. 오늘은 CSS Variables 시스템을 개발한 과정에 대해 이야기 할 것이다.
#C8102E를 외워야 된다고?
나두아이오에서 컴포넌트를 스타일링 하려면 프로퍼티 에디터로 스타일을 수정하거나 직접 CSS를 작성할 수 있었다. 하지만 반복되는 값을 저장해두고 사용하는 시스템이 없어, 매번 똑같은 값을 작성해줘야 된다는 불편함이 있었다.
CSS Variables 시스템을 구축하면 반복해서 사용하는 값은 변수에 저장해두고, 스타일링할 때 사용할 수 있다.
구조
나두아이오는 Editor가 App을 감싸고 있는 구조인데, 이 App은 iFrame으로 불러온다.
Editor와 App은 Bridge로 통신하고, CSS Variables의 경우 App에서 수정될 일이 없기 때문에 Editor에서 App으로 데이터를 보내는 단방향 구조를 사용한다.
App은 styled-jsx
를 통해 string으로된 CSS를 resolve하여 jsx로 주입하는 형식을 사용하기 때문에, 브라우저에서 지원하는 CSS Variables를 그대로 사용하기로 했다.
const { className, styles } = useMemo(
() =>
styledCss.resolve`${css}`,
[ css ],
);
return (
<Fragment>
{styles}
...
</Fragment>
);
CSS Variables는 특정한 section
이나 글로벌에 변수를 선언하고 해당하는 범위 내에서 변수를 사용할 수 있는 시스템이다.
section {
--primary-color: #C8102E;
}
:root {
--secondary-color: #00B2A9;
}
개발자 도구를 열면 이런 식으로 확인할 수 있다.
지원하는 브라우저는 다음과 같다.
요구사항
- CSS Variables를 프로젝트별로 저장하고, Editor에서 편집할 수 있다.
- App에 주입되는 CSS에
var(--variable)
문법을 사용하면 CSS Variables가 적용된다.
Editor는 어떤 변수명에 어떤 값이 매칭되는지만 알면 되고, App의 :root에 CSS Variables를 저장한다.
구현
1. Editor: CSS Variables 생성과 수정 패널
UI 구현은 단순하다. 동적으로 추가/삭제 가능한 폼을 만들고, 저장하는 API와 연동한다.
예약어, 문법, 중복 등 validation 처리한다.
2. App: root 엘리먼트에 변수 주입
일반적으로 .css 파일을 통해 :root에 변수를 저장하지만, 파일을 통해 주입하는 것보다 App의 root 엘리먼트에 접근하여 CSS 변수를 입력하는 방식이 간결하다고 생각했다.
- App의 설정들을 다루는 파일에 Provider를 추가하고, 저장한 cssVariables를 받아온다.
- root 엘리먼트를 선택한다.
- root 엘리먼트 스타일에
setProperty
로 변수를 주입한다.
export const NadooAppCSSVariablesContextProvider = ({
cssVariables = {},
children,
}: NadooAppCssVariables & { children: ReactNode }) => {
useEffect(() => {
const root = document.querySelector(':root') as HTMLElement;
Object.entries(cssVariables).forEach(([key, value]) => {
root?.style.setProperty(key, value);
});
}, [cssVariables]);
return (
<NadooAppCssVariablesContext.Provider
value={{ cssVariables }}
>
{children}
</NadooAppCssVariablesContext.Provider>
);
};
App에 CSS Variables가 설정되어 바로 사용할 수 있다.
3. App: 변수 수정/삭제
Editor에서 Variables를 편집하면 기존에 등록됐던 프로퍼티가 제거돼야 한다. 데이터베이스에서는 이전 변수를 따로 저장하고 있지 않으므로, 렌더링될 때마다 기존에 root 저장돼있던 프로퍼티를 제거하고 다시 추가하는 매커니즘을 구현했다.
렌더링에 영향을 미치지 않도록 이전 변수를 저장하는 데에 ref를 사용했다.
- 렌더링 이후 effect가 실행되면 ref에서 이전의 cssVariables를 가져와 root에서 제거한다.
- 새로운 cssVariables를 프로퍼티에 세팅한다.
- ref에 현재 cssVariables를 저장한다.
const previousCssVariablesRef = useRef(cssVariables);
useEffect(() => {
const previousCssVariables = previousCssVariablesRef.current;
const root = document.querySelector(':root') as HTMLElement;
Object.entries(previousCssVariables).forEach(([key, value]) => {
root?.style.removeProperty(key);
});
Object.entries(cssVariables).forEach(([key, value]) => {
root?.style.setProperty(key, value);
});
previousCssVariablesRef.current = cssVariables;
}, [cssVariables]);
@media (max-width: var(–sm-breakpoint))
CSS Variables를 사용하는 개발자 대부분이 이런 문법을 본 적이 없을 것이다. 왜냐하면 틀린 문법이기 때문이다.
미디어 쿼리에서는 CSS Variables를 사용할 수 없다. 하지만 Theme System에서 브레이크포인트는 핵심 기능이기 때문에 반드시 지원해야 했다.
그래서 찾은 대안이 SASS이다. SASS는 CSS 컴파일러로, CSS 문법을 간결하고 다채롭게 쓸 수 있게 돕는다. SCSS와 SASS는 약간의 문법 차이가 있는데, SASS 컴파일러로 두 문법 모두 커버 가능하다.
SCSS Playground로 변수를 선언하고 미디어 쿼리에서 사용해보았다.
sass 라이브러리를 사용하여 직접 root 엘리먼트에 접근하지 않고, scss로 작성된 string을 css로 변환하여 jsx로 resolve하여 사용한다.
- sass 모듈을 불러온다.
- cssVariables를
name: value; ...
형식의 string으로 변환한다. - variablesString과 css를 합친 뒤
sass.compileString
을 통해 css로 컴파일한다. - 컴파일한 css를 jsx로 resolve한다.
4-1. 컴파일 과정에 오류가 있는 경우 기존의 cssString만 리턴하여 resolve한다.
const sass = require('sass');
const compileSCSS = (variablesString: string, cssString: string) => {
try {
return sass.compileString(variablesString + '\n' + cssString).css;
} catch (error) {
return cssString;
}
};
const variablesCss = useMemo(
() =>
Object.entries(cssVariables)
.filter(([key]) => key.startsWith('$'))
.map(([key, value]) => `${key}: ${value};`)
.join('\n'),
[cssVariables],
);
const { className, styles } = useMemo(
() =>
styledCss.resolve`${compileSCSS(variablesCss, css)}`,
[variablesCss, css],
);
return (
<Fragment>
{styles}
...
</Fragment>
);
provider를 통해 직접 dom에 접근할 필요가 없어 매커니즘이 간결해졌고, 미디어 쿼리를 지원할 수 있게 되었다. 또한 Editor에서도 sass를 이용해서 validation 처리를 하여, CSS 수정 과정에서 문법 오류가 있는 경우 핸들링해주어 기존에 부족했던 기능을 보완했다.
이번 시간엔…
오늘은 노코드 툴을 개발하는 이유와 사용자들에게 주고 싶은 가치, 그리고 CSS Variables 시스템을 설계하고 구현한 내용을 이야기했다.
입사 후에 처음으로 큰 시스템 구축을 맡으면서 App과 Editor 시스템을 더 잘 이해하게 되었고, CSS Variables와 SASS에 대해서도 알 수 있어 많은 것을 남긴 작업이었다.
다음 시간엔…
CSS Editor에서 자동완성으로 CSS Variables를 더욱 편하게 사용할 수 있게 보완하였고 그 내용을 담으려고 한다.