주변기기
Sofle
그렇다. 결국에는 스플릿 키보드에 입문해버리고 말았다. 원래 생각하던 키보드는 Bastard Keyboards에서 판매하는 Charybdis였다. 그러다가 Sofle로 첫 스플릿 키보드를 시작하게 된 데에는 몇 가지 이유가 있다.
- 내가 스플릿 키보드를 잘 쓸 수 있을지 아닐지도 모르는 상황에서 바로 구매하기에는 가격대가 있는 편이었다. 거기다가 공식적으로는 무선을 지원하지 않아서 이를 무선으로 쓰려면 부품들 몇 가지를 따로 사서 조립해야 한다.
- Charybdis는 어차피 Bastard Keyboards에서 도면을 공유하고 있어서 어디서 사든 폼팩터 자체는 같을 수밖에 없다. 따라서 ZMK로 무선을 돌리는 타오바오산 키보드를 사는 것도 꽤나 괜찮은 방법인 것 같았다.
- 만일 스플릿 키보드를 산다면 나는 Magic Trackpad가 있으므로 두 스플릿 키보드 사이에 트랙패드를 놓고 쓸 수 있고, 그럼 Charybdis의 가장 큰 특징인 트랙볼이 사실상 있을 필요가 없어진다.
그래서 나는 우선 상대적으로 저렴한 Sofle을 산 후, Sofle과 트랙패드 조합을 우선 사용해보고 쓸만하면 Charybdis를 휴대용을 겸하여 타오바오에서 사기로 했다.

내가 산 제품은 Sofle v2 기반으로, 여기서 thumb cluster의 1.5u 키캡을 1u키캡으로 바꾸고 오른손의 EC11 인코더를 조이스틱으로 바꾼 모델이다. 개인적으로 양쪽 모두에 인코더가 있는 오리지널 Sofle은 두 인코더를 모두 사용하기가 상당히 난해하다고 생각하던 차라서 고민없이 구매했다.

며칠 사용해 본 소감으로는, 우선 ZMK는 사용하기 편하다. QMK는 시작하려면 QMK MSYS를 깔거나 Homebrew에서 QMK를 깔아야 하는데 ZMK는 GitHub Action으로 모든 것이 이루어져 새로운 무언가를 설치할 필요가 없다. 물론 환경은 한 번 구축하면 되는 것이라 이것만으로 ZMK가 QMK보다 낫다고 할 수는 없겠지만.
키보드가 스플릿인 것은 보통 B키 문제가 가장 큰데, 나는 이를 위해 키보드가 오기 전부터 세벌식을 연습하고 있었다. 또 그러다보니 타자 속도가 잘 나오지 않아서 한글타자의 경우에는 문제를 느끼기 힘들었다. 오히려 문제는 영어타자에서 발생했는데, 영어타자는 스플릿 자체는 별 문제가 없었지만 ortholinear 배열이다보니 이 부분에서 오타가 많이 났다.
키의 경우는 우선 thumb cluster를 제외하고 알파열만 보면 절대적으로 부족한 것은 맞다. 왼쪽부터 차례로 배열한다면 위의 두 열에서는 두 개의 키가 빠지게 되고, 셋째 열에서는 하나의 키가 빠지게 된다. 이는 불편한 일이기는 하지만 참지 못할 정도는 아니다. 가령, 나중에 키맵에서 ZMK를 다룰 때 제대로 살펴보겠지만, 나는 숫자 키들에 auto-shift를 켠 상태로, 9 키와 0 키를 각각 빠르게 두 번 누르면 [와 ]이 나오도록 했고, 가장 오른쪽 위에는 위의 사진과 같이 -를 넣되 마찬가지로 빠르게 두 번 누르면 =이 나오도록 했다. 이 자리에 있어야 하는 백스페이스는 왼손 thumb cluster로 옮겼다. 그 외에도 여러 수정이 있고, 지금도 써 가면서 바꾸고 있지만 어쨌든 핵심은 이 정도는 그렇게까지는 불편하지 않다는 것이다.
마지막으로 조이스틱이 예상보다 쓸만하다. 조이스틱을 마우스 커서로 쓰는 레이어도 만들어두기는 했지만 트랙패드도 있고, 또 마우스를 대체하기에는 급한대로 쓸 수는 있지만 권장할 만하지는 않다. 오히려 이를 방향키로 쓰는 것이 너무 편했는데, HHKB같은 미니배열에서는 보통 Fn + ], Fn + ;, Fn + ', Fn + /을 방향키로 대체하는지라 새끼손가락의 부담이 조금 있는 반면 조이스틱은 엄지로 사용하게 되므로 그럴 일이 없었다.
이후 여러 수정을 거쳐 현재의 키맵은 다음과 같다.
#define ZMK_POINTING_DEFAULT_MOVE_VAL 1200 // 600
#define ZMK_POINTING_DEFAULT_SCRL_VAL 25 // 10
#include <input/processors.dtsi>
#include <zephyr/dt-bindings/input/input-event-codes.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/bt.h>
#include <dt-bindings/zmk/keys.h>
#include <dt-bindings/zmk/rgb.h>
#include <dt-bindings/zmk/pointing.h>
#include <dt-bindings/zmk/outputs.h>
#define AS(keycode) &as LS(keycode) keycode // Autoshift Macro
&mmv_input_listener { input-processors = <&zip_xy_scaler 2 1>; };
&msc_input_listener { input-processors = <&zip_scroll_scaler 2 1>; };
&msc {
acceleration-exponent = <1>; // 0
time-to-max-speed-ms = <100>; // 300
delay-ms = <0>; // 0
};
&mmv {
time-to-max-speed-ms = <1000>;
acceleration-exponent = <1>;
trigger-period-ms = <16>;
};
/ {
scroll_encoder: scroll_encoder {
compatible = "zmk,behavior-sensor-rotate";
#sensor-binding-cells = <0>;
bindings = <&msc SCRL_DOWN>, <&msc SCRL_UP>;
tap-ms = <100>;
};
rgb_encoder: rgb_encoder {
compatible = "zmk,behavior-sensor-rotate";
#sensor-binding-cells = <0>;
bindings = <&rgb_ug RGB_BRI>, <&rgb_ug RGB_BRD>;
};
macros {
leader: snippet_leader {
compatible = "zmk,behavior-macro";
#binding-cells = <0>;
tap-ms = <1>;
wait-ms = <1>;
bindings = <¯o_tap &kp LC(LG(LA(SPACE))) &kp LC(LG(LS(RBKT)))>;
};
leader_alt: snippet_leader_alternative {
compatible = "zmk,behavior-macro";
#binding-cells = <0>;
tap-ms = <1>;
wait-ms = <1>;
bindings = <¯o_tap &kp LC(LG(LA(SPACE))) &kp LC(LG(LS(LA(RBKT))))>;
};
};
combos {
compatible = "zmk,combos";
combo_enter {
timeout-ms = <200>;
key-positions = <56 60>;
bindings = <&kp ENTER>;
};
combo_leader_alternative {
timeout-ms = <200>;
key-positions = <57 59>;
bindings = <&leader_alt>;
};
combo_vivaldi {
timeout-ms = <150>;
key-positions = <62 63>;
bindings = <&kp LG(LALT)>;
};
combo_left_rectangle {
timeout-ms = <150>;
key-positions = <53 54>;
bindings = <&kp LC(LALT)>;
};
combo_lbkt {
timeout-ms = <150>;
key-positions = <9 10>;
bindings = <&kp LBKT>;
};
combo_win {
timeout-ms = <150>;
key-positions = <53 63>;
bindings = <&tog_on 2>;
layers = <0>;
};
combo_mac {
timeout-ms = <150>;
key-positions = <53 63>;
bindings = <&tog_off 2>;
layers = <2>;
};
};
behaviors {
as: auto_shift {
compatible = "zmk,behavior-hold-tap";
#binding-cells = <2>;
tapping_term_ms = <275>;
quick_tap_ms = <0>;
flavor = "tap-preferred";
bindings = <&kp>, <&kp>;
};
mta: mod_tap_alternative {
compatible = "zmk,behavior-hold-tap";
#binding-cells = <2>;
tapping_term_ms = <250>;
quick_tap_ms = <0>;
flavor = "tap-preferred";
bindings = <&kp>, <&kp>;
};
lta: layer_tap_alternative {
compatible = "zmk,behavior-hold-tap";
#binding-cells = <2>;
flavor = "hold-preferred";
tapping-term-ms = <200>;
bindings = <&mo>, <&kp>;
display-name = "Layer-Tap-Alt";
};
lts: layer_tap_sticky {
compatible = "zmk,behavior-hold-tap";
#binding-cells = <2>;
flavor = "hold-preferred";
tapping-term-ms = <200>;
bindings = <&kp>, <&slq>;
display-name = "Layer-Tap";
};
slq: sticky_layer_non_quick_release {
compatible = "zmk,behavior-sticky-key";
#binding-cells = <1>;
bindings = <&mo>;
release-after-ms = <1000>;
ignore-modifiers;
};
gtesc: grave_tilde_escape {
compatible = "zmk,behavior-mod-morph";
#binding-cells = <0>;
bindings = <&kp ESC>, <&kp GRAVE>;
mods = <(MOD_LGUI|MOD_LALT|MOD_LSFT|MOD_RGUI|MOD_RALT|MOD_RSFT)>;
keep-mods = <(MOD_LSFT|MOD_RSFT)>;
};
bspc_del: backspace_delete {
compatible = "zmk,behavior-mod-morph";
#binding-cells = <0>;
bindings = <&kp BACKSPACE>, <&kp DELETE>;
mods = <(MOD_LALT|MOD_LSFT|MOD_RALT|MOD_RSFT)>;
};
tog_on: toggle_layer_on_only {
compatible = "zmk,behavior-toggle-layer";
#binding-cells = <1>;
display-name = "Toggle Layer On";
toggle-mode = "on";
};
tog_off: toggle_layer_off_only {
compatible = "zmk,behavior-toggle-layer";
#binding-cells = <1>;
display-name = "Toggle Layer Off";
toggle-mode = "off";
};
};
keymap {
compatible = "zmk,keymap";
MacBase {
bindings = <
>esc &kp N1 &kp N2 &kp N3 &kp N4 &kp N5 &kp UP_ARROW &kp N6 &kp N7 &kp N8 &kp N9 &kp N0 &kp MINUS
&kp TAB &kp Q &kp W &kp E &kp R &kp T &kp DOWN_ARROW &kp Y &kp U &kp I &kp O &kp P &kp BACKSLASH
&mta CAPS RALT &kp A &kp S &kp D &kp F &kp G &kp LEFT_ARROW &kp H &kp J &kp K &kp L &kp SEMI &kp APOS
<s LSHFT 5 &kp Z &kp X &kp C &kp V &kp B &kp RIGHT_ARROW &kp N &kp M &kp COMMA &kp DOT &kp FSLH <a 4 ENTER
&kp C_MUTE &tog_on 4 &kp LEFT_ALT &kp LEFT_GUI &kp SPACE &bspc_del &none &leader &kp SPACE &kp LCTRL &kp RALT &mo 1
>;
sensor-bindings = <&inc_dec_kp C_VOLUME_UP C_VOL_DN>;
display-name = "MacBase";
};
MacFn {
bindings = <
&kp C_BRI_DEC &kp C_BRI_INC &kp LC(UP) &kp LG(SPACE) &kp LG(LS(N4)) &kp LG(LS(N5)) &trans &kp C_PREV &kp C_PP &kp C_NEXT &kp C_MUTE &kp C_VOL_DN &kp C_VOL_UP
&out OUT_TOG &bt BT_NXT &bt BT_CLR &trans &kp LC(LG(LA(R))) &trans &trans &trans &trans &trans &trans &kp LC(LG(LA(P))) &trans
&rgb_ug RGB_SPI &rgb_ug RGB_HUI &rgb_ug RGB_SAI &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans
&rgb_ug RGB_SPD &rgb_ug RGB_HUD &rgb_ug RGB_SAD &kp LC(LG(LA(C))) &trans &kp LC(LG(LA(B))) &trans &kp LC(LG(LA(N))) &trans &trans &trans &kp LC(LG(LA(FSLH))) &trans
&rgb_ug RGB_TOG &rgb_ug RGB_EFF &trans &trans &trans &trans &trans &tog_on 2 &trans &trans &trans &trans
>;
sensor-bindings = <&rgb_encoder>;
display-name = "MacFn";
};
WinBase {
bindings = <
>esc &kp N1 &kp N2 &kp N3 &kp N4 &kp N5 &kp UP_ARROW &kp N6 &kp N7 &kp N8 &kp N9 &kp N0 &kp MINUS
&kp TAB &kp Q &kp W &kp E &kp R &kp T &kp DOWN_ARROW &kp Y &kp U &kp I &kp O &kp P &kp BACKSLASH
&mta CAPS RALT &kp A &kp S &kp D &kp F &kp G &kp LEFT_ARROW &kp H &kp J &kp K &kp L &kp SEMI &kp APOS
<s LSHFT 5 &kp Z &kp X &kp C &kp V &kp B &kp RIGHT_ARROW &kp N &kp M &kp COMMA &kp DOT &kp FSLH &mt RSHFT ENTER
&kp C_MUTE &tog_on 4 &kp LEFT_ALT &kp LEFT_GUI &kp SPACE &bspc_del &none &leader &kp SPACE &kp LCTRL &kp RALT &mo 3
>;
sensor-bindings = <&inc_dec_kp C_VOLUME_UP C_VOL_DN>;
display-name = "WinBase";
};
WinFn {
bindings = <
&kp F1 &kp F2 &kp F3 &kp F4 &kp F5 &kp F6 &trans &kp F7 &kp F8 &kp F9 &kp F10 &kp F11 &kp F12
&out OUT_TOG &bt BT_NXT &bt BT_CLR &trans &trans &trans &trans &trans &trans &trans &kp LBKT &kp RBKT &trans
&rgb_ug RGB_SPI &rgb_ug RGB_HUI &rgb_ug RGB_SAI &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans
&rgb_ug RGB_SPD &rgb_ug RGB_HUD &rgb_ug RGB_SAD &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans
&rgb_ug RGB_TOG &rgb_ug RGB_EFF &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans
>;
sensor-bindings = <&rgb_encoder>;
display-name = "WinFn";
};
Navigation {
bindings = <
&trans &trans &trans &trans &trans &trans &mmv MOVE_UP &trans &trans &trans &trans &trans &trans
&trans &trans &kp UP_ARROW &trans &trans &trans &mmv MOVE_DOWN &trans &mkp LCLK &mkp RCLK &trans &trans &trans
&trans &kp LEFT_ARROW &kp DOWN_ARROW &kp RIGHT_ARROW &trans &trans &mmv MOVE_LEFT &trans &trans &trans &trans &trans &trans
&trans &trans &trans &trans &trans &trans &mmv MOVE_RIGHT &trans &trans &trans &trans &trans &trans
&trans &tog_off 4 &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans
>;
label = "Nav";
sensor-bindings = <&scroll_encoder>;
};
Symbol {
bindings = <
&trans &trans &trans &trans &trans &trans &mmv MOVE_UP &trans &trans &trans &kp LBKT &kp RBKT &kp EQUAL
&trans &trans &trans &trans &trans &trans &mmv MOVE_DOWN &trans &trans &trans &trans &trans &trans
&trans &trans &trans &trans &trans &trans &mmv MOVE_LEFT &trans &trans &trans &trans &trans &trans
&kp LSHFT &trans &trans &trans &trans &trans &mmv MOVE_RIGHT &trans &trans &trans &trans &trans &trans
&trans &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans &trans
>;
label = "Sym";
sensor-bindings = <&rgb_encoder>;
};
};
};
각 레이어에서 가운데 있는 것들 (첫째 레이어 기준 &kp ARROW 시리즈가 있는 열)은 조이스틱이고, 가장 아래 열의 좌측 1번은 왼쪽 스플릿의 노브 클릭에 해당되는 키다. 이 노브의 다이얼은 각 레이어의 sensor-bindings로 지정해주면 되고, 대부분은 volume으로 뒀지만 예외적으로 Navigation 레이어에서는 스크롤 역할을 하게 해 보았다. 어차피 조이스틱으로 마우스 대체하는 것은 너무 힘든 일이라 이 레이어 자체를 쓸 일은 없지만, 최후의 보험 같은 것이다.
레이어는 총 여섯 개다. MacBase와 WinBase가 각각 macOS와 Windows용 기본 레이어이고 그 위에 MacFn과 WinFn이 하나씩 붙으며, 나머지 둘은 방금 전, 마우스 역할을 하는 Navigation과, 9, 0 키에 중괄호, 대괄호를 배정해주는 Symbol 레이어다. Mac과 Windows 사이의 전환은 맨 아랫줄 왼쪽에서 두 번째 키와 오른쪽 끝 키를 함께 누르는 combo가 맡는데, MacBase에서 누르면 tog_on 2로 WinBase를 켜고 WinBase에서 같은 자리를 누르면 tog_off 2로 끄기 때문에 하나의 조합으로 왕복이 된다. 반면 Fn 레이어는 오른쪽 맨 끝 키에 &mo로 걸어두어 누르고 있는 동안만 올라간다. WinFn은 그냥 숫자 키를 펑션 키로 바꿔주는 것에 불과하고, MacFn은 매직키보드의 기능을 따라하도록 넣어놨다.
키가 모자란 자리는 대부분 hold-tap과 mod-morph로 메웠다. 왼쪽 Shift는 길게 누르면 Shift지만 Tap behavior는 Symbol 레이어가 1초간 sticky로 걸린다. 따라서, 가령 대괄호 [를 누르고 싶으면 이를 tap하고 9 키를 누르면 되고, 중괄호 {를 누르고 싶으면 이를 double tap하되, 나중 tap은 hold한 상태로 (첫째 tap에서 Symbol 위치로 이동하고, 둘째 tap은 Symbol 레이어에서 해당 위치가 LSHFT이므로 이를 누르고 있는 것이다) 9 키를 누르면 된다. 오른손 새끼손가락의 Enter는 길게 누르면 Navigation이 된다. Esc 자리는 mod-morph여서 그냥 누르면 Esc, 아무 modifier나 얹으면 `가 나오되 Shift는 그대로 통과해 ~가 된다. 왼손 thumb cluster로 옮긴 Backspace도 같은 방식이라 Alt나 Shift를 얹으면 Delete가 되고, 홈열 맨 앞의 Caps Lock 자리는 톡 치면 오른쪽 Option (한영키), 길게 누르면 Caps Lock이다.
Combo는 몇 개 되지 않는다. 양쪽 스페이스를 동시에 누르면 Enter가 나오는 콤보는 우하단 shift의 Tap을 엔터로 설정하면서 잘 쓰지 않는다. 세벌식에서 중요한 키인 [를 매번 Symbol 레이어에서 불러올 수는 없으니 숫자열의 8과 9를 같이 누르면 레이어를 거치지 않고도 [가 나온다. 나머지는 macOS 쪽 앱을 겨냥한 것으로, Rectangle과 Vivaldi가 쓰는 modifier 조합을 그대로 combo에 얹어두었다. &leader 매크로는 hyper 조합 두 개를 연달아 보내 스니펫을 부르는 데 쓰고 있고, 그 옆의 두 키를 같이 누르면 변형인 &leader_alt가 나간다. 현재는 VS Code로 갈아타기는 했는데, 어쨌든 아이디어와 키 자체는 서브라임 텍스트에서 소개한 것과 동일하다.
댓글남기기