2023년 9월부터 이마고웍스에서 AI 기반 치과 CAD/CAM SaaS인 Dentbird의 프론트엔드를 맡고 있습니다. 구독·결제, 3D 뷰어, 외부 CAM 연동처럼 외부 의존이 많은 복잡한 화면을 만듭니다. AI로 코드가 빠르게 바뀌는 환경에서도 안전하게 개발할 수 있도록 아키텍처·디자인 패턴·상태관리와 검증 체계를 설계하는 것이 일의 중심입니다. 동시에 그 프론트엔드의 기반이 되는 모노레포·환경 분리·빌드·품질 자동화 같은 플랫폼 토대까지 한 제품 안에서 폭넓게 다뤄 왔습니다. 아래는 그중 대표적인 작업들의 배경·판단·회고입니다.
React
TypeScript
Next.js
TanStack
Query
Three.js
Electron
Playwright
Jest
Docker
AWS
GitHub
Actions
Datadog
2023.09 ~ 2025.07 ·
React · TypeScript · FSD
성과 — CaseInfo 다이얼로그가 3개 앱에 중복돼 Compound Component로 단일화, 약 66%(984→330줄) 감소. FSD·디자인 패턴의 도입 기준을 팀에 정립
계정·구독 클라이언트를 입사 초기부터 오래 맡았는데 코드 컨벤션이 구체적으로 정해져 있지 않았습니다.
components·hooks 같은 역할 단위 폴더라
만들기는 쉬우나 그 훅이 어디서 왜 쓰이는지 드러나지 않음구독제 전환으로 기존 로직과 분리된 기능을 새로 시작할 기회가 생겨 팀과 공감대를 이룬 뒤 FSD를 도입했습니다.
{Domain}API + query key factory, DTO와 도메인을 잇는
mapper 분리적용 사례는 둘입니다. CaseInfo 다이얼로그는 3개 앱에 중복돼 있어 Compound + FormProvider 단일 컴포넌트로 추출해 약 66%(984→330줄) 줄였습니다. 통신 방식(Custom Protocol과 HTTP)은 나중에 한쪽으로 굳히기 쉽도록 전환 지점만 추상화하되 약간의 중복은 의도적으로 허용했습니다.
한계가 분명했습니다. FSD를 도입했는데도
widgets·pages처럼 UI 단위가 레이어에 섞여 “이
파일을 어디 넣을지”가 풀리지 않았습니다. 그걸 정하느라 작은 논의가
이어졌습니다. 디자인 패턴도 “코드 일관성”이라는 명분으로 굳이 필요하지
않은 곳에 적용한 경우가 많았습니다. 제안한 방향을 팀원들이 다르게 이해해
같은 패턴을 쓰는 방법이 갈라지기도 했습니다. 가장 크게 깨달은 것은
디자인 패턴이 어떤 문제를 푸는지 정확히 이해하지 못한 채 도입했다는
점입니다.
그래서 지금은 기술을 도입할 때 우선순위 높은 문제를 먼저 정의하고 대안을 비교해 맞을 때만 팀이 같은 이해를 가진 상태에서 도입합니다. 효용뿐 아니라 도입 비용(커뮤니케이션·마이그레이션·과도기 복잡도)까지 따져야 한다는 것도 이때 알았습니다. 구조 자체가 나빴던 것은 아닙니다. 그 코드는 모노레포로 옮겨간 뒤에도 남아 있습니다. 이 작업에서 배운 것은 ’좋은 구조’보다 ’도입의 전제’였습니다.
2025.10 ~ 2026.06 ·
React · TanStack Query · IntersectionObserver · Context API
성과 — 운영 중 발생한 무한 재요청 회귀를 공유 캐시 구조로 근절(재마운트돼도 중복 요청 없음을 회귀 테스트로 고정), 썸네일을 지연 로딩과 요청 배치로 최적화해 “N장 = N요청” 구조를 해소
케이스 목록은 사용자가 가장 자주 보는 화면이고 썸네일이 수십 장 표시됩니다.
onError 폭주를 억제하는 재시도 1회
제한을 보조 장치로 배치, 성공 시 실패 횟수를 초기화하면 같은
영구 오류가 반복되므로 의도적으로 초기화하지 않음무한 재요청은 재시도 횟수만 제한해도 멈춥니다. 하지만 그것은 증상이고 진짜 원인은 재마운트마다 훅이 새로 생겨 캐시가 소실되고 같은 오류를 다시 호출하는 구조였습니다. 재마운트 자체를 없애는 것은 더 큰 공사라 공유 캐시로 우회하는 실용적 선택을 했습니다. 증상을 덮을지 구조를 고칠지 사이에서 비용 대비 효과가 가장 큰 지점을 고른 작업입니다.
설계에서 하나 더 신경 쓴 것은 실패를 다루는 방식입니다. 보통 캐시는 성공만 담지만 여기서는 403 같은 영구 오류도 짧게 캐시했습니다. 실패를 기억하지 않으면 재마운트마다 같은 오류를 다시 호출하기 때문입니다.
2023.09 ~ 2025.07 ·
React · TypeScript · TanStack Query
성과 — 모든 에러를 한 덩어리로 처리하던 구조를 예측 가능/불가능 에러로 분리해 한 기능의 실패가 전체 페이지를 중단시키지 않도록 개선(BusinessLogicError·ErrorBoundary 체계는 모노레포 이관 후에도 존속)
일회성 크레딧에서 반복 구독으로 전환하는 시점에 구독·결제 화면(플랜 업그레이드·좌석 구매·취소·재개·쿠폰·결제수단·히스토리)을 전담했습니다. 당시 조직에는 에러 처리 정의가 없었습니다.
HTTP 200 · success:false · errorCode로 내려주는 비즈니스
에러는 실패가 아니라 의도된 결과이므로 폴백 UI로 뭉뚱그리지 않음BusinessLogicError 커스텀 클래스로 throw하고
.when(errorCode, action).otherwise() 체인으로 분기에러 처리는 정교한 도구에서 출발하지 않았습니다. 에러를 구분 짓는
일이 먼저였습니다. 예측 가능한 실패와 불가능한 실패를 나누지 않으면 잘
만든 ErrorBoundary를 두어도 결국 같은 폴백으로 처리됩니다. 이 구조는
모노레포로 이관된 뒤에도 BusinessLogicError와
withErrorBoundary HOC로 남아 있습니다.
다만 조직 차원의 에러 처리 정의는 여전히 없습니다. 무엇이 실패해도 무엇은 살아남아야 하는가를 결제 데이터 의존 관계까지 끊어내는 설계는 당시 출시 우선순위와 설계 역량의 한계로 끝까지 가지 못했습니다. 이 문제의식은 이후 관측·복원력 표준화에서 조직 차원으로 이어갔습니다. 서버가 에러 응답 형식을 HTTP 200에 success:false로 내려줄지 4xx·5xx로 내려줄지부터 합의가 필요했습니다. 에러 처리는 프론트엔드 혼자 정할 수 없고 서버와의 계약이 전제였습니다.
2026.04 ~ ·
React · TypeScript · Datadog RUM
성과 — RUM에서 한 줄로 뭉쳐 보이던 실패 124건을 원인별로 추적 가능하게 직렬화, 5개 앱 에러 처리를 공통 표준으로 정렬
에러는 잡히지만 어디서 왜 발생했는지 추적되지 않았습니다.
new Error('failedToExportMeshes') 한 줄로 collapserender-time 에러는 react-error-boundary로 처리하지만
비동기 에러에는 이 라이브러리를 쓰지 않았습니다.
대신 비동기 에러는 에러 객체 자체에 metadata를 담는 방식(표준
Error.cause와 구조화된 실패 상세)을 택했습니다. throw
지점은 한 줄로 두고 RUM에는 grep 가능한 단일 라인으로 직렬화했습니다.
공통 라이브러리라 한 곳을 고치면 5개 앱에 반영됩니다.
에러 판별도 라이브러리에 종속되지 않게 분리했습니다.
instanceof로 판별하면 라이브러리마다 타입이 달라 분기가
복잡해집니다. 에러 객체의 형태(shape)만 보고 HTTP 상태를
추출해 axios·fetch·커스텀 wrapper 어디서 온 에러든 같은
기준으로 다뤘습니다. 이렇게 뽑은 상태로 에러를 일시적 오류·치명적
오류·무시할 오류 등 여섯 갈래로 나눠 분류별로 대응했습니다. 이 분류
로직은 한 앱에서 만든 뒤 공통 라이브러리로 옮겼습니다.
관측은 에러를 잡는 일에 그치지 않고 왜 발생했는지 추적할 수 있게 만드는 일이었습니다. 한 줄로 뭉쳐 보이던 124건을 원인별로 가른 것이 그 출발이었습니다.
안전망 설계에서 가장 크게 배운 것은 최후 안전망일수록 외부 의존이 없어야 한다는 점입니다. 처음엔 ErrorBoundary의 fallback UI에 회사 디자인 시스템을 그대로 썼습니다. 디자인 시스템에 큰 변경이 생기자 그 fallback UI 자체가 에러를 발생시키며 흰 화면만 남았습니다. 그래서 최상위 fallback은 의존성을 최대한 제거한 UI로 다시 만들어 가장 바깥 레이어에 뒀습니다. 직접 만든 7분류 에러 체계가 특정 앱에 치우쳐 있던 것도 인정하고 팀 공통 표준에 맞춰 폐기·정렬했습니다.
2026.04 ~ ·
Three.js · draco3d · WebGL · Playwright · pngjs
성과 — 시각 회귀 검증을 이벤트 기반 대기로 바꿔 케이스당 120초 → 37.7초, 이미지 맞춤 미세조정 약 1,420줄을 Three.js 표준 기능으로 정리
사내 3D 라이브러리를 Three.js로 옮기는 마이그레이션이 AI 기반으로 빠르게 진행됐습니다. 그 과정에서 “기존과 똑같이 보이게” 두 엔진의 렌더 결과 이미지를 맞추는 방식으로 구현된 부분이 남았습니다.
썸네일이 본 화면과 미묘하게 다르던 것도 legacy 픽셀을 맞추려 남긴 보정값이 그사이 통일된 기준과 갈라진 탓이라 같은 원칙을 적용했습니다.
VTK는 modeler·crown·milling의 핵심 렌더 엔진입니다. 그중 썸네일 생성 부분만 Three.js로 통합하자는 제안이 있었습니다.
이 경계는 약 일주일 뒤 전제가 바뀌며 수명을 다했습니다. 썸네일 생성이 서버로 오프로드되면서(Node 환경엔 canvas가 없어 VTK가 빈 이미지를 생성) batch 썸네일이 Three.js로 완전 통합됐습니다. ’안 하는 결정’은 그 시점의 전제에 묶인 판단입니다. 전제가 바뀌면 다시 뒤집는 것이 맞다는 점까지 포함해 기록으로 남겼습니다.
이 케이스에서 담당한 부분은 마이그레이션 결과물의 이미지 맞춤 부채를 식별·정리하고 회귀를 검증하는 작업입니다(현재 진행). 결과 이미지를 맞추는 빠른 마이그레이션은 미세조정에 기대어 취약합니다. 소스 엔진의 동작을 이해해 타깃 엔진의 표준 기능으로 옮겨야 조건이 바뀌어도 어긋나지 않습니다.
2023.09 ~ 2025.10 ·
Next.js · i18n · Fastify · MongoDB
성과 — 타입세이프 i18n으로 번역 키 오류를 런타임이 아닌 빌드 타임에 차단, 성능·SEO를 포함한 기업 랜딩 v3를 풀스택 단독 리뉴얼
입사 첫 업무로 기업 랜딩을 단독 리뉴얼해 이후 2년 넘게 화면부터 서버 API까지 운영했습니다.
locales/en)을 단일 소스로 타입
정의를 자동 생성tsc --noEmit에서 즉시 검출“런타임에야 드러날 문제를 빌드 타임으로 당긴다”는 원칙은 이후 결제·테스트·관측까지 일하는 방식의 공통 축이 됐습니다.
다만 타입 체크가 만능은 아니었습니다. 그 점은 뒤늦게 배웠습니다. 같은 방식을 본 앱에 넓힐지 검토하며 그동안의 i18n 버그를 직접 집계해봤습니다. 정작 가장 흔한 ’번역 키 노출’은 영어 단일 소스로 만든 타입 체크가 잡지 못하는 부분이었습니다. 언어별 키 누락이라 더 싸고 정확한 방법은 언어 간 키 parity 체크였습니다. 도구는 좋아 보이는가가 아니라 무엇을 막는가를 데이터로 따져 선택해야 한다는 것을, 직접 도입했던 방법을 스스로 점검하며 확인했습니다.
2024.07 ~ 2025.12, 이후 단독 운영 ·
Electron · React · TypeScript · Datadog
성과 — 좌표계·전달 방식이 제각각인 외부 CAM 12종을 단일 연동 인터페이스로 통합, 앱마다 다르던 설치 감지 타임아웃을 3초로 맞추고 비차단 안내로 통일, Windows 전용 CAM을 Mac에서 재현하는 검증 도구로 원인을 우리 쪽과 CAM 쪽으로 구분
사내 연동 앱을 단독으로 설계하고 운영했습니다. Dentbird에서 디자인한 보철 케이스를 사용자 PC의 외부 CAM 소프트웨어로 보내는 앱입니다. 연동은 파일을 전달하는 데이터 채널과 앱 설치·실행 여부를 아는 감지 채널 두 축으로 나뉩니다. 이 케이스는 둘을 모두 다룹니다.
여러 통신 방식을 구현 속도가 아니라 장기 안정성 기준으로 비교했습니다.
| 방안 | 검토 결과 |
|---|---|
| WebSocket 로컬 서버 | 구현은 가장 빠르나 곧 LNA 적용 대상 → 1~2년 후 재차단될 단기 해법, 탈락 |
| HTTPS 로컬 서버 | 인증서·배포 부담 + 같은 LNA 사정 → 탈락 |
| Custom Protocol | HTTP 요청이 아니라 LNA 적용 대상이 아님 → 채택 |
연동한 CAM 중 하나에서 보철 가장자리(마진)가 물결처럼 어긋나 들어가는 문제가 접수됐습니다. 이 CAM은 Windows 전용이고 3D 뷰포트가 GPU 렌더라 Mac에서는 결과를 볼 수 없었습니다.
두 번째 축인 감지 채널입니다. 브라우저는 LNA 정책 때문에 로컬 헬스체크로 앱 실행을 직접 확인할 수 없습니다. 창 포커스 변화 같은 휴리스틱으로 추정할 수밖에 없습니다. 두 방향의 오탐이 있었습니다.
false-negative — 앱이 이미 실행 중이면 OS가 기존 창으로 라우팅해 포커스 변화가 없어 ’미설치’로 오인 → 설치돼 있는데 설치 모달 표시
false-positive — 로딩 중 사용자가 DevTools를 열어 포커스가 빠지면 ’설치됨’으로 오인 → 미설치인데 다운로드 안내 억제
감지를 완벽하게 만들지 않고 틀려도 사용자가 막히지 않는 쪽으로 정책 통일
batch 2.5초·Linker 10초로 달랐던 감지 타임아웃을 3초로 일치. 미감지면 미설치로 간주하되 설치 안내를 항상 비차단(non-trapping)으로 표시해 어떤 경우에도 다운로드가 가능하게 함
DevTools로 인한 false-positive는 타임아웃으로 막지 못하는 한계임을 인지한 채 수용
근본 해법으로는 Zoom·Slack 같은 설치형 서비스처럼 감지 판정 자체를 없애고 다운로드 안내를 항상 제공하되 설치돼 있으면 자동 실행되는 방식을 제안해 기획·QA와 합의했습니다. 아직 착수 전입니다.
처음엔 로컬 서버 방식이 핵심이었지만 정책 변화로 더는 쓸 수 없게 됐습니다. 직접 폐기한 뒤 Custom Protocol 기반으로 옮겼습니다. “당장 되는 방법”이 아니라 “정책이 또 바뀌어도 흔들리지 않는 방법”을 골랐습니다. 이 프로젝트에서 가장 중요한 판단이었습니다. 브라우저·OS의 실행환경 정책(LNA, 포커스, 프로토콜 핸들러)이 웹과 로컬의 경계에서 무엇을 막고 허용하는지를 이 과정에서 깊이 이해했습니다.
감지에서도 같은 태도를 얻었습니다. 휴리스틱은 늘 틀릴 수 있으므로 감지를 완벽하게 만들려 애쓰기보다 틀려도 사용자가 막히지 않도록 설계하는 쪽이 더 단단했습니다. 같은 문제가 다시 열리면서 휴리스틱 위에 보강을 더하는 방식으로는 구조적 한계를 넘지 못한다는 것을 겪었습니다. 그래서 감지 판정 자체를 없애는 방향까지 제안했습니다.
2026.07 · Electron · TypeScript · MUI
성과 — 곧 사용이 끊길 뻔한 협력사 인앱을 엔진·로그인·의존 정리로 되살리고 우리 서비스가 바뀌어도 재빌드가 필요 없는 구조로 전환 (실제 Electron과 CDP로 검증 완료, 배포 대기)
협력사 Medit의 소프트웨어 안에서 Dentbird를 쓸 수 있도록 Electron 인앱 형태로 제공했지만 2023년 이후 유지보수되지 않았습니다.
get-user) 응답이
200이면 로그인 성공으로 판단했고 이 API를 레거시로 제거하자 판단이
무너짐인앱은 C++ 애드인과 함께 빌드되는 구조라 재빌드 자체가 부담이었습니다. 그래서 되살리면서 우리 쪽 구현과의 결합을 줄였습니다. 이제는 서비스의 구현 아키텍처가 바뀌지 않는 한 인앱을 다시 빌드할 일이 없습니다. 결합을 완전히 없애는 데 집착하기보다 큰 변경이 있을 때만 드물게 재빌드하면 되는 선까지 끊는 것을 택했습니다.
두 문제는 해결됐고 릴리스는 협력사 Medit이 맡아 배포를 기다리고 있습니다.
2023.09 ~ 2025.10 ·
NX · Module Federation · iframe · postMessage
성과 — 4개 서비스에 중복된 공통 기능 4종을 same-origin iframe 단일 구현으로 합쳐 4개월간 유지, 배포 단위와 조직 제약이 바뀔 때마다 통합 방식을 다시 선택
4개 서비스(cloud·crown·modeler·milling)가 공통 기능 4개(설정·내보내기·탐색기·뷰어)를 공유했고 이 기능의 변경 비용이 컸습니다.
Module Federation도 후보였지만 다른 기능에 적용해본 경험상 초기 설정이 복잡하고 모노레포가 아닌 소비처에서 부담이 커 이 건에서는 제외했습니다. 몰라서가 아니라 적용해 본 뒤 비용 대비 이득으로 판단한 것입니다. console-client에는 Module Federation을 직접 적용했습니다.
같은 문제를 라이브러리 → iframe → 빌드타임 통합 → 다시 iframe으로 세 번 다르게 풀었습니다. 매번 그때 더 낫다고 본 방법을 택했지만 운영해보면 조직·배포 제약이 다른 답을 가리켰습니다. 빌드타임 통합은 기술적으로 깔끔했어도 사내 배포 주기와 맞지 않아 결국 same-origin iframe으로 돌아왔습니다. 이 과정에서 “정답 기술”은 없고 제약에 맞는 선택만 있다는 것을 배웠습니다. 더 나아 보이는 방법이라도 운영해보기 전에는 알 수 없었습니다.
이 통합은 4개월간 공통 기능 4종을 단일 구현으로 유지했고 이후 Vite로 표준이 통일되며 그쪽으로 넘어갔습니다. 별개로 사내 공통 디자인 시스템(MUI 기반)에도 DatePicker 확장(닫힘 핸들러 덮어쓰기 버그 수정·placeholder·클릭 핸들러 노출) 등 공통 컴포넌트 기여와 v3.0.0 릴리스 관리를 했습니다.
2024.06 ~ ·
TypeScript · NX · Git Subtree · i18n
성과 — 조직 변경에 맞춰 앱 2종·라이브러리 6종을 커밋 이력 보존하며 NX 모노레포로 이관하고 버전이 어긋난 공용 라이브러리는 빌드타임에 합쳐 통합 배포·버전 정합을 확보, 잔존 설정이 유발한 OOM도 진단해 빌드 태스크 11→9로 정리
분리됐던 팀들이 합쳐지는 조직 변경이 있었고 레포 구조가 그 변화와 맞지 않았습니다.
webpack.config.js가 남아 NX 자동 타겟 추론을 유발했고
여기서 불필요한 산출물과 OOM이 생기는 것을 진단해 빌드 태스크
11개 → 9개, OOM 해소분산됐던 레포를 하나로 합치니 여러 곳에 걸친 변경을 한 PR에서 한눈에 보고 리뷰할 수 있게 됐습니다. npm 패키지나 외부 모듈로 따로 배포되다 보니 버전이 어긋나 불일치가 생겼습니다. 이것도 빌드 타임에 함께 컴파일하는 방식으로 사라졌습니다.
“무엇을 합칠까”보다 합친 뒤에도 흔들리지 않을 토대(도메인 통합 라이브러리)를 먼저 마련한 것이 다음 단계인 격리 재현 환경의 기반이 됐습니다.
2025.11 ~ ·
Docker · Playwright · Claude · 런타임 Config · EC2 · qase
성과 — 명세 검증을 AI로 전량 자동화하려다 비결정성·비용의 한계를 확인하고 격리 재현 위 E2E로 자리 잡음. 정성 평가로는 문제없던 테스트가 실제로는 결함을 잡지 못한다는 것을 mutation testing으로 확인. 그 뒤 배포마다 검증을 통과해 나간 결함을 추적하는 CFR 대시보드를 직접 만들어 팀에서 운영 중
AI 도입으로 코드 변경 속도가 빨라진 만큼 회귀도 함께 늘었습니다.
격리 재현 환경은 앞서 직접 만든 도메인 통합 라이브러리와 런타임 환경 분리 위에서, 특정 커밋 시점의 클라이언트·서버·DB를 그대로 Docker로 띄우는 구조입니다.
이렇게 했는데도 예정된 배포가 품질 기준을 통과하지 못해 밀렸습니다. AI가 만든 테스트를 다시 AI가 평가한 탓이었습니다. 겉으로는 문제없어 보여도 실제로는 결함을 잡지 못하는 테스트가 많았습니다.
도구를 여럿 거쳤어도 결정성, 그리고 무엇이 개선되고 무엇이 나빠지는지 숫자로 보는 일이 핵심이었습니다. AI는 테스트를 빠르게 만들어 주지만 AI가 만든 것을 AI가 평가하면 못 잡는 결함이 그대로 남습니다. 측정하지 않으면 그것을 끝내 알 수 없습니다. 좋은 테스트는 구현이 아니라 의도한 명세를 검증해야 한다는 점도 이 과정에서 분명해졌습니다.
AI 변경 감지에도 한계가 있었습니다. 어떤 회귀는 오히려 AI 리뷰가 통과시킨 잘못된 테스트에서 나왔습니다. 기획 의도가 코드에도 테스트에도 없이 티켓 코멘트에만 있었던 탓입니다. 직접 바로잡았고 AI를 판단에 쓸수록 그 판단이 맥락 없이 틀릴 수 있다는 전제까지 함께 설계해야 한다는 것을 배웠습니다. EC2 상시 구동의 보안·안정성 부담을 줄이려 변경 감지는 이후 로컬 데일리 E2E로 옮겼습니다.
2024 ~ ·
Electron · electron-builder · electron-updater · GitHub Actions · Azure Pipelines · S3
성과 — 데스크톱 앱 빌드 시간 33~39분 → 17~24분, 업로드 아티팩트 757MB → 334MB(약 56%), Windows 코드 서명을 빌드머신 단독 운영으로 전환(인프라팀 의존 제거)
두 데스크톱 앱(Batch·Linker)의 빌드·서명·배포가 특정 PC와 사람에 종속돼 있었습니다.
운영하면서 두 장애도 직접 겪었습니다. 상시 가동 self-hosted 러너가
빌드 tarball을 /tmp에 남기는데 정리에서 누락돼 나흘간
738개·약 26GB가 쌓여 8개 앱 빌드가 전부 멈춘 적이 있습니다.
/ 디스크는 여유였는데 /tmp만 별도 RAM
파티션이라 진단이 늦었고 정리 단계를 산출물 생성과 대칭으로 맞춰 근본
해결했습니다. GitHub Actions 이관 중에는 Windows 서명 도구 압축 해제가
심볼릭 링크 권한 부족으로 조용히 5회 실패하던 것을 러너 Developer Mode로
해결했습니다.
자동 업데이트가 조용히 실패하던 문제를 추적하다 무결성 검증이 “빌드 시점에 기록된 해시 = 실제 바이너리 해시”라는 전제 위에 선다는 것을 알게 됐습니다. 증상이 아니라 전제를 건드리는 결정(어디서 서명할 것인가)이 곧 업데이트 안정성을 좌우합니다.