![[실전 성능 05] 눌렀을 때 반응하기](https://img.thenullpage.com/posts/5759/5759_1_a7f3bd.webp)
지난 편까지 화면이 빨리 뜨고 흔들리지 않게 만드는 법을 다뤘다. 그런데 잘 뜨고 안정된 화면도 눌렀을 때 반응이 없으면 사용자는 답답해한다. 이번 편은 반응성, 즉 사용자가 무언가를 누른 뒤 화면이 응답하기까지의 시간이다. 요즘 가장 많은 사이트가 통과하지 못하는 지표라 나도 여기서 오래 씨름했다.
1. 반응성이란 무엇을 재는가
반응성 지표는 사용자가 클릭이나 탭, 키 입력 같은 상호작용을 했을 때 다음 화면이 갱신되기까지 걸린 시간을 잰다. 사람이 버튼을 눌렀는데 아무 변화가 없으면 눌렸는지 아닌지 알 수 없어 불안해진다. 그 침묵의 시간이 200밀리초를 넘어가면 사람은 지연을 또렷이 느끼고, 반응성이 나쁘다고 판단한다. 짧아 보이지만 상호작용마다 쌓이면 사이트 전체가 굼떠 보였다. 한 번의 지연은 참아도 누를 때마다 반복되는 굼뜸은 사이트가 원래 이렇게 느린 곳이라는 인상을 남겼고, 그 인상은 쉽게 지워지지 않았다.
이 지표가 까다로운 이유는, 로딩이 끝난 뒤 사용자가 조작하는 내내 계속 측정된다는 점이었다. 첫 화면만 잘 만들면 되는 게 아니라, 페이지에 머무는 동안 일어나는 모든 상호작용이 대상이었다. 그래서 사용자가 가장 많이 누르는 지점, 예를 들어 목록의 정렬 버튼이나 좋아요 같은 곳이 느리면 지표가 크게 나빠졌다. 나는 자주 눌리는 곳부터 반응성을 점검했다.
중요한 건 사용자가 기다리는 건 최종 결과가 아니라 반응의 신호라는 점이었다. 버튼을 눌렀을 때 색이 살짝 바뀌거나 로딩 표시가 뜨기만 해도 사람은 눌렸다고 안심한다. 진짜 데이터가 오는 건 그다음이어도 괜찮았다. 나는 이 차이를 이용해서, 무거운 작업이 걸린 곳도 최소한 즉각적인 반응 신호는 먼저 주도록 설계했다.
반응성은 특히 저사양 기기에서 티가 났다. 내 좋은 컴퓨터에서는 즉각적이던 조작이, 오래된 폰에서는 눈에 띄게 굼떴다. 처리 능력이 약한 기기일수록 자바스크립트 실행에 시간이 더 걸리기 때문이다. 그래서 이 지표야말로 내 화면이 아니라 느린 기기의 현장 데이터로 봐야 진실이 보였다.
2. 메인 스레드가 막히는 이유
반응성의 근본 원인은 대부분 하나로 모였다. 브라우저가 화면을 그리고 사용자 입력을 처리하는 통로가 하나뿐인데, 그 통로가 다른 일로 막혀 있으면 입력에 반응할 수가 없었다. 무거운 자바스크립트가 이 통로를 오래 붙잡고 있으면, 사용자가 아무리 눌러도 브라우저는 순서가 올 때까지 응답을 미뤘다. 이게 반응 지연의 핵심 구조였다.
내 사이트에서도 페이지가 처음 뜨는 순간이 특히 위험했다. 그 시점에 여러 스크립트가 한꺼번에 실행되며 통로를 오래 점유했고, 하필 그때 사용자가 뭔가를 누르면 반응이 크게 늦었다. 사람들은 페이지가 뜨자마자 조작하려는 경향이 있어서, 바로 그 순간의 지연이 지표를 갉아먹었다. 나는 초기 실행 부담을 줄이는 데 집중했다. 화면이 다 그려진 것처럼 보이는데 실은 뒤에서 스크립트가 아직 돌고 있는 그 애매한 구간이, 사용자에게는 가장 조작하고 싶으면서도 반응이 없는 함정 구간이었다.
해결의 방향은 통로를 오래 붙잡지 않는 것이었다. 한 번에 오래 도는 무거운 작업을 잘게 쪼개서, 중간중간 브라우저가 입력을 처리할 틈을 주는 방식이다. 작업을 조각내면 총 시간은 비슷해도 사용자 입력이 끼어들 여지가 생겨 반응성이 좋아졌다. 나는 긴 반복 작업을 발견하면 덩어리를 나누는 것을 우선 고려했다.
또 하나는 당장 필요 없는 실행을 뒤로 미루는 것이었다. 첫 화면과 무관한 스크립트가 초기에 통로를 차지하고 있으면, 그것부터 나중으로 밀었다. 사용자가 조작할 가능성이 높은 초반에 통로를 비워두는 것이 반응성에 직접적으로 도움이 됐다. 미루기는 로딩뿐 아니라 반응성에도 효과가 있었다.
3. 즉각적인 신호 먼저 주기
모든 작업을 빠르게 만들 수는 없었다. 어떤 조작은 본질적으로 서버를 다녀와야 해서 시간이 걸렸다. 이럴 때 핵심은 결과가 오기 전에 반응의 신호부터 주는 것이었다. 버튼을 누른 즉시 눌린 상태를 표시하고 로딩 표시를 띄우면, 실제 결과가 조금 늦어도 사용자는 시스템이 살아 있다고 느꼈다.
예를 들어 좋아요 같은 조작은 서버 응답을 기다리지 않고 화면부터 먼저 바꿨다. 사용자가 누르는 순간 숫자가 오르고 색이 변했고, 서버 처리는 뒤에서 조용히 진행됐다. 만약 서버가 실패하면 그때 되돌리면 됐다. 이렇게 화면을 먼저 낙관적으로 갱신하니 체감 반응성이 극적으로 좋아졌다. 사용자는 기다림을 거의 느끼지 못했다. 실제 서버 왕복 시간은 그대로였지만 사용자가 인식하는 시간은 거의 0에 가까워졌는데, 결국 반응성은 실제 속도만큼이나 인식의 문제라는 걸 이 기법이 잘 보여줬다.
다만 이 낙관적 갱신은 조심스럽게 써야 했다. 결과가 뒤집힐 수 있는 조작에 남발하면, 잠깐 됐다가 도로 취소되는 어색한 장면이 나왔다. 그래서 나는 실패 확률이 낮고 되돌리기 쉬운 조작에만 이 기법을 적용했다. 반응성을 위해 정확성을 희생하지 않는 선을 지키는 것이 중요했다.
신호를 주는 것만으로 부족할 때는 기대치를 관리했다. 오래 걸리는 작업이면 진행 정도를 보여주거나, 최소한 지금 처리 중이라는 표시를 유지했다. 사람은 얼마나 걸릴지 알면 훨씬 잘 기다렸다. 침묵이 불안을 키우니, 무슨 일이 일어나고 있는지 계속 알려주는 것이 반응성 관리의 일부였다.
4. 무거운 계산 옮기기
때로는 상호작용 자체가 무거운 계산을 불렀다. 복잡한 목록을 정렬하거나 큰 데이터를 가공하는 조작은 그 자체로 통로를 오래 막았다. 이런 계산은 가능하면 사용자가 조작하기 전에 미리 해두거나, 조작 시점에는 최소한만 하도록 줄였다. 무거운 일을 조작의 순간에서 떼어내는 것이 관건이었다.
서버에서 할 수 있는 계산은 서버로 넘겼다. 브라우저에서 큰 데이터를 정렬하느니, 서버가 이미 정렬해서 보내주면 브라우저는 그리기만 하면 됐다. 앞 편에서 말한 미리 계산해두기 원리가 여기서도 통했다. 사용자의 기기, 특히 약한 기기의 부담을 서버로 옮기면 반응성이 좋아졌다. 서버는 한 번 계산해 여러 사용자에게 재활용할 수도 있었다.
브라우저 안에서도 무거운 계산을 본래 통로 바깥으로 보내는 방법이 있었다. 화면과 무관한 순수 계산은 별도의 작업 공간에서 돌려 메인 통로를 비워둘 수 있었다. 나는 이 방식을 아주 무거운 처리에만 제한적으로 썼는데, 구조가 복잡해지는 대가가 있었기 때문이다. 필요할 때만 꺼내 쓰는 카드였다.
결국 반응성 최적화는 통로를 비워두는 여러 방법의 조합이었다. 잘게 쪼개고, 미루고, 미리 계산하고, 신호를 먼저 주고, 무거운 건 옮겼다. 어느 하나의 마법은 없었고, 자주 눌리는 지점마다 이 도구들을 상황에 맞게 골라 썼다. 나는 측정으로 가장 굼뜬 조작을 찾아 그곳부터 손봤다.
5. 반응성은 배려의 문제
반응성을 파고들며 느낀 건, 이 지표가 결국 느린 기기를 쓰는 사람에 대한 배려라는 점이었다. 좋은 장비를 가진 사람은 웬만한 지연을 못 느낀다. 반응성이 나쁠 때 가장 고통받는 건 오래된 폰이나 약한 기기를 쓰는 사용자였다. 이들을 챙기는 것이 곧 반응성을 챙기는 일이었다.
그래서 나는 개발할 때 일부러 느린 기기를 흉내 냈다. 브라우저 도구로 처리 속도를 강제로 낮추고 조작해보면, 평소엔 안 보이던 굼뜬 지점이 드러났다. 좋은 기기에서만 확인하고 넘어갔다면 그 사용자들은 계속 방치됐을 것이다. 나쁜 조건에서 테스트하는 습관이 반응성을 지키는 안전장치였다.
반응성은 또한 사이트의 인상을 좌우했다. 눌렀을 때 즉각 반응하는 사이트는 잘 만들어진 도구처럼 느껴졌고, 굼뜬 사이트는 어딘가 고장 난 느낌을 줬다. 속도와 안정성이 첫인상과 신뢰였다면, 반응성은 사이트를 쓰는 내내 이어지는 품질감이었다. 세 지표가 함께 갖춰져야 비로소 쾌적했다. 하나라도 무너지면 나머지 둘이 아무리 좋아도 사용자는 그 무너진 지점의 불편만 기억했고, 그래서 세 축을 골고루 통과선 위에 올려두는 것이 중요했다.
이렇게 핵심 세 지표를 모두 살펴봤다. 다음 편부터는 이 지표들을 실제로 망가뜨리는 구체적인 요소들을 하나씩 파고든다. 첫 대상은 이미지다. 앞에서 잠깐 언급했던 이미지가 화면을 밀어내는 문제를, 이번엔 한 편을 통째로 할애해 원인과 해법을 깊이 뜯어보겠다.