← Back to Projects

DERMA:CODE

A consultation platform that turned intuition-led skin consultations into a data-driven workflow.

CompanyGaia Labs
RoleFrontend Early design & branding
TeamFrontend 1 Backend 1 Design 1
PeriodNov 2024 - Present
StatusTheia Gangnam & Myeongdong In production

A consultation tool that captures the face with a 3D scanner, runs AI skin analysis, and simulates recommended treatment combinations and their expected results. Consultants run their sessions on this software. I owned the entire frontend, and design and branding through V1. Once the simulation became a full feature, I wrote the V2 product design and requirements spec first and brought in a designer so I could focus on the engine. In use at Theia Gangnam, and extended to the Myeongdong clinic in July 2026.

Problems and solutions

Problem

I queried the skin-analysis statistics accumulated since the clinic opened with a script and split them by age group and sex. Most patients' scores were packed into a narrow range.

Judgement

Absolute scores could not explain the differences between patients, so I switched to showing each patient's relative position within their cohort.

I proposed a Q1/Q3 quartile-based formula to the leadership. After applying it, I re-ran the statistics, confirmed that the score concentration had eased, and adopted it as the final calculation.

Solution

Mean-based normalisation still left most patients in a narrow band with no visible difference. I interpolated linearly between p10 and p90 markers to place each patient within the cohort, and used 25/50/75 quartile cut-offs to classify severity into four levels. Age groups with too few samples fell back to the parent cohort, and unknown sex fell back to the combined cohort.

The normalisation is a temporary correction for in-product display; I judged the root cause to be model performance. I shared the analysis and it became a company-level model problem definition.

Result

The DERMA:CODE model is currently being retrained, and I am taking part in building the annotation tool (human-in-the-loop) that produces the training data.

Problem

"I want to see the result before the treatment" kept coming up in VoC. The need was clear, but nobody had confirmed whether deforming a 3D face per treatment was actually possible, so proposing the feature felt premature.

Judgement

Rather than turning the request straight into a feature proposal, I checked feasibility and cost first.

On day one I tested how far a 3D file could be deformed to gauge the scope. Over the next two weeks I built a prototype that deforms the 3D face mesh per treatment and verified it produced the intended result on screen.

Solution

Modelled the treatment effect with about six deformation parameters, and moved the per-treatment values out into backend data managed from a separate settings page.

This separated the engine logic from the treatment definitions, so adding a new treatment no longer requires touching the main 3D processing engine.

Result

With the working prototype as evidence I proposed promoting it to a full feature, and it was adopted. It now lives inside the product as Derma:Mirror and has been used in marketing content and in a due-diligence demo for investors.

Problem

V1 ran with five GNB tabs, and new features meant the structure had to be revisited. Which features consultants actually used during sessions, and which screens they centred on, had never been confirmed.

Judgement
Tab usage over two months%
Main tab84
Upselling tab4.5
I calculated tab usage from two months of event data and cross-checked the consultation flow with Hotjar heatmaps.

I computed per-tab usage from event data and checked the real consultation flow with Hotjar heatmaps.

Over two months, the tab for the main feature was used 84% of the time, while the tab split out for upselling reached only 4.5%. The heatmaps also showed consultants continuing inside the solution-recommendation flow rather than moving to a separate upselling tab.

Solution

I proposed removing the upselling tab from the GNB and, instead of adding new features as new tabs, surfacing synergy solutions inside the recommendation flow.

Rather than splitting the consultation into separate tabs, the V2 GNB and screen structure were reorganised so that additional solutions follow on within the existing recommendation flow.

Result

V2 dropped the dedicated upselling tab and restructured the tabs around the added features.

Problem

The pore, wrinkle and redness corrections in the treatment simulation were processing a 4096×2048 texture, about 8M pixels, with Canvas 2D on a single CPU thread. Changing a single parameter froze the screen.

Judgement
Same effect, two implementationsms
Canvas 2D (CPU pixel loop)1,117
WebGL (GPU shader)50
Both implementations used the same 4096×2048 input texture and effect in an internal comparison page.
Where 243ms of GPU processing for one effect wentms
upload 5.5draw 0.3readback 218.7
Total 243 · 18.5ms remaining across other work
I instrumented one production effect after moving it to the GPU and split the processing window into upload, draw and readback.

First I implemented the same effect on Canvas 2D and on the GPU and compared processing time. Canvas 2D took 1117ms and the GPU 50ms, roughly 22×, and the outputs matched, so I decided to move to the GPU.

After moving one real effect to the GPU I measured each stage again. Of 243ms in total, GPU compute was only 0.3ms while the CPU readback took 218.7ms. The bottleneck was not GPU compute but getting the data back from GPU to CPU.

Solution

Instead of processing each effect on the GPU and reading back every time, I restructured the pipeline to chain several effects on the GPU and read back once at the end.

While instrumenting, I also found a CPU bug: applyRednessCorrection() was missing its vertexUVs argument and processing the whole area instead of the face region. Fixing it cut that stage from 17.4s to 5.1s.

Result
Processing time for one correctionseconds · lower is better
  1. –Before migration22.3
  2. 1Some effects on GPU17.4
  3. 2One-line CPU bug fixmissing vertexUVs5.1
  4. 3GPU chain, one readback3.2
최초단계최종

Improving the GPU pipeline and the CPU logic together brought the full simulation from 22.3s down to 3.2s.

Problem

The consultation screen parses and renders the scanner's .dwb 3D mesh and textures in the browser, and its LCP reached 16.7 seconds. Checking the render path with Lighthouse and DevTools showed three things delaying the first paint: a render gate waiting on the diagnosis API, unused fonts on the critical path, and 3D resource loading.

Judgement

Instead of changing several things at once, I split the work by cause and measured stage by stage, confirming each change's effect on LCP before the next optimisation.

Solution
LCP by stageseconds · lower is better
  1. –First measurement16.7
  2. 1Render gate removed14.2
  3. 2Unused fonts removed2,827ms request removed · no LCP change14.2
  4. 3Loading screen added23.7롤백
  5. 42D first + lazy 3D7.9
  6. 5Server resize + WebP3,506KB → 123KB5.7
최초단계최종악화 · 롤백
I measured the same consultation screen with Lighthouse and Chrome DevTools after each change.

1. Removed the render gate → LCP 14.2s

2. Removed an unused Geist font request that took 2,827ms → it loaded in parallel with other resources, so LCP remained at 14.2s

3. Added a loading screen → LCP worsened to 23.7s. Measurement showed the overlay was hiding the LCP element; rolled back

4. Rendered 2D content first and lazy-loaded 3D resources → 7.9s

5. Resized images with Sharp on the server and converted to WebP → resources 3,506KB → 123KB, final 5.7s

Result
16.7s→5.7s−66%

LCP went from 16.7s to 5.7s, about 66% lower. I published the whole process on my blog, including each stage's measurements and the rollback of the failed loading screen.

↗ Five-stage measurement log (blog, Korean)

Tech Stack

Next.js 15React 19TypeScriptReact Three Fiberthree.jsMediaPipeTensorFlow.jsKonvaTanStack QueryZustandreact-i18nextTailwind 4