먼저 이 글을 쓴 이유는 부트캠프
를 진행하다가, 크*
님께서 하신 말씀을 듣고 남기고 싶어졌다.
크*
: 이런 고민을 하시는게 좋아보여요!!
이런걸로 학습하고 글로 써봐도 좋을 것 같아요~
꼭 쓰라는건 아니구~
뭔가 쓰라는 것 같은 느낌.....
이렇게 말씀하신 것도 있었지만, 많이 고민했던 내용이라 다른 사람에게 내가 고민한 내용을 공유하고 싶어졌다.
이번 부트캠프의 과제는 프레임워크, 라이브러리 없이 순수 JS
로만 투두 리스트 만들기
였다. 여기서 요구사항 중 Store객체
를 쓰라는 것이 있었다.
처음에 Store객체...?? 이건 뭐야 싶었다. 알아보니... 다음과 같았다.
웹 프론트엔드에서 Store객체는 Client딴에서 State를 관리하는 것을 말한다.
이게 무슨 말인가?
React
같은 웹 프론트엔드 프레임워크를 사용해봤다면 Redux, Recoil
같은것을 써봤을 것이다. 이러한 것들이 Client State를 관리하기 위한 Store 객체이다.
우리는 이곳에다가 백엔드에서 받아온 정보를 저장해두기도 하고, 버튼이 클릭되었다거나 뭐 이런 Status 값을 기억해두기도 한다.
사실 Store객체가 왜 필요해? 라고 묻는다면 버튼 누름 상태, 안누름 상태 이런 걸 백엔드한테 보내주고 관리할껀가???
=> 뭘 그렇게 까지.... 그런건 프론트에서 저장해두고 처리해~ 에서 나온게 Store객체라고 생각하면 좋을 것 같다.
한마디로 정리하면 프론트엔드 개발에서만 필요한 State를 프론트엔드 딴에서 저장할 필요가 있고, 이 때문이 만들어졌다고 할 수 있다.
그러면 Store객체는 단순히 Client State를 저장해둔다. 이게 끝일까? 그렇지 않다.
보통 Store객체들은 State에서 변화가 일어나면 그 State와 관련된 UI를 다시 랜더링 시킨다.
이건 무슨말이지..?? Store의 예시를 하나 들어보면서 이야기 해보겠다. 그러면 이해가 쉬울 것이다.
Store의 구현 방법은 다양하지만 Flux구조의 예시를 들어가며 Store
의 정의를 해보려한다.
그림만 보면 이해가 안가니 예시를 들어보자. 아래와 같은 UI가 있다고 쳐보자. 이 UI는 박스 추가를 하면 박스가 한개 더 생기고, 개수도 하나 추가된다. 이 상황을 위의 구조에 대입하여 설명해보겠다.
Dispatch
: 사용자가 박스 추가 버튼 클릭이라는 Action
발생Reducer
: 추가했으니 Client State를 업데이트 해줄게~SubScribe + Listener
: State가 변경되었잖아?? 그러면 이 State가 변화될때 변화되어져야 하는 UI의 State를 업데이트 해줄게!! 이런 식으로 이루어진다. 참고로 Redux
도 이런식으로 구현되어져 있다.
대부분의 Store들은 State를 Store 한곳에서 관리함으로써 사용자 액션에 따라 State를 변경, 변경에 따른 UI랜더링 등을 한곳에서 진행하려 한다. 이를 통해 얻고자 하는 이점이 뭘까?
다른 곳에서도 막 바꾸면 너무 정신없잖아~
위에서 부르지말고 필요한 곳에서만 불러~
일단 됐다 치고 빨리 보여줘~
1,2번까지는 이해가 가는데, 3번에서 낙관적 업데이트
는 무엇인가?
인스타그램에 좋아요
를 누르면 어떻게 작동할까?
바로 하트가 채워지는 애니메이션이 출력되면서 좋아요가 즉각적으로 완료된다.
인스타그램의 기술이 대단해서 좋아요 요청을 보내고 응답이 오는 시간이 짧아서 이렇게 즉각적으로 되는걸까? 그렇지 않다.
스마트폰 데이터를 꺼보고 좋아요를 누르면 좋아요가 찍힌다. 그리고 데이터를 키고 다시 재접속하면 좋아요가 취소되어져 있는 것을 볼 수 있다.
어... 그러면 서버 응답과는 관계없이 일단 좋아요가 찍히는거네? 이런 결론을 낼 수 있다.
맞다. 서버의 응답과는 관계없이 일단 Client State를 변화시켜주는 방법이 낙관적 업데이트이다.
쉽게 말해 일단 됐다 치고해~
마인드이다.
서버에게 요청을 보내긴 하나, 그 시간을 기달리지 않고 일단 됐다 치고 Client State에 변화를 주고 관련 랜더링 함수를 실행시켜주는 것이다.
이를 통해 사용자들은 보다 자연스러운 UX경험을 받을 수 있다.
하지만 단점은 없을까...?? 물론 있다.
됐다 치고 했는데, 요청이 실패하면? Client State와 Server State가 다른 경우가 생긴다. 이는 사실 중요한 데이터였다면 매우 치명적인 상황이다.
사실 이번 프로젝트에서 Store객체 요구사항을 처음 받았을때, 가장 고민했던 점은 Store객체를 사용해서 얻는 이점에 대해 고민했다.
이 이점에 대해 고민하고 이 이점을 가장 잘 살릴 수 있는 개발을 해보고 싶었다.
위에서 말했던 Store객체를 구현함으로써 얻는 이점에서 1, 2번은 구현함에 따라 자연스럽게 얻어지는 이점이었고, 3번의 경우에는 내가 어떻게 개발하냐에 따라 달라지는 이점이었다.
낙관적 업데이트를 하지 않을 경우, 서버에 요청을 보내고 이를 응답으로 받은 뒤에 State를 그에 따라 변화를 준다. 이에 따라 3번의 이점은 의미가 없어진다.
Store객체의 강점을 최대한 살리는 구현을 위해 낙관적 업데이트, 비관적 업데이트 방식 중에 어떤식으로 구현할지에 대해 고민을 했는데, 그때 고민 사항은 다음과 같다.
- 본 프로젝트에서 궁극적으로 얻으려는 게 뭘까?
- 본 프로젝트에서 Store 객체를 적용해보라는 이유가 뭘까?
- 이 프로젝트에서 데이터의 정확도가 그렇게 중요한가?
- 사용자는 어떤 방식이 자연스럽다고 여길까?
본 프로젝트가 데이터의 정확도 보다는 Store 객체가 무엇인지 정확히 알고 이에 대한 강점을 살리는게 중요하다고 판단했다.
또한 기존에 했던 방식이 비관적 업데이트 방식이라서 색다르게 낙관적 업데이트 방식을 한번 해보고 싶기도 했다.
또한 비관적 업데이트 방식으로 구현을 한번 해봤었는데, 한번 상에 당연히도 딜레이가 있었다. 이에 따라 화면에 깜빡임이 발생했고, 사용자가 느끼기에 매우 부자연스러움이 느껴졌다.
이번 프로젝트에서 드래그 앤 드롭이 있었는데, 이동에 따라 랜더링을 시킬때 화면 상에 깜빡임이 나타났다. 너무 이상해보였다….
이에 따라 이번 프로젝트에선 낙관적 업데이트
방식으로 개발해보기로 결정했다.
내가 이번 프로젝트에서 구현한 간단한 프로젝트 아키텍처인데, 여기서 이 블로깅와 가장 관련있는 Store객체를 자세히 봐보자.
여기서 집중있게 봐야되는 점은 액션이 발생하고 Reducer를 통해 State Update를 해주고 있는 점이다. 내 설계 구조를 보면 Store에 바로 Client Update를 해주고 Server에도 따로 Update를 해주고 있다.
이를 통해 일단 성공했다고 치고 Client State를 액션에 따라 Update를 해줬고 리스너에 들어있는 랜더링 함수를 빠르게 발생시켰다.
그러면 비관적 업데이트 방식은 어떻게 이루어질까?
이게 비관적 업데이트 방식으로 Server에 변화를 먼저주고 응답이 오면 Client State에 변화를 준다. 이렇게 되면 UI에 State변화에 따른 랜더링을 시키는데 너무 오래걸린다.
나는 낙관적 방식으로 개발함으로써 사용자에게 빠른 UX경험을 제공했다.
음… 뭐가 더 맞냐?? 이거에 대한 답은 없는 것 같다. 상황에 따른 적용?? 이게 정답인 것 같다.
대신에 같이 한번 생각 해볼거리가 있다.
낙관적 업데이트의 가장 큰 단점은 요청이 이상이 생겨서 Server State와 Client State의 오차가 생기는 경우이다.
이를 해결해줄 순 없을까? 답: 있다.
어떻게 해결할 수 있을까?
그 정답은 다음과 같은 구조로 만들면 된다.
쉽게 말해, 일단 됐다고 치고 할꺼긴 한데… 일단 Server 응답오면 맞는지 체크는 해볼게!! 틀리면 고쳐주고~
이런식으로 부정확도에 대한 한계를 개선할 수 있다.
하지만… 좀 이상한게 있다.
빠른 UX를 위해 낙관적 업데이트를 도입했는데, 요청에 실패가 나서 갑자기 좋아요 하트가 눌러진게 몇초 있다가 갑자기 없어진다면?? 사용자 입장에선 이게 더 이상한 UX가 될 것이다.
낙관적 업데이트와 비관적 업데이트 중에 뭐가 더 정답이냐? 음.. 정답은 없다.
단… 이번에 낙관적 업데이트를 도입함으로써 이 개발 방식은 정말 조심해야 되는 방식이란걸 깨달은것 같다.
위와 같이 개발을 마치고 크* 님께 왜 이를 도입했고, 어떻게 개발했는지 말씀드리면서 이에 대한 이야기를 해봤다. 말씀하신 내용은 정리하면 다음과 같다.
낙관적 방식은 데이터 중요도가 높지 않고, 빠른 UX가 중요할때 보통 쓰여요. 은행 같은 데이터 중요도가 높은 서비스에선 절대 쓰이지 않는 방식이죠.
인스타그램 좋아요 같이 중요하지 않은 정보이고, 빠른 UX가 중요하다보니 이러한 개발 방식을 채택하죠. 보통 좋아요 찍고 다른 곳으로 빨리 이동하잖아요? 이런 경우가 아니라면 사용을 신중히 해야돼요. 그래도 Store에 대해 깊이 있게 고민하고 이를 적용해보려고 한 점이 매우 좋네요.
나도 낙관적 업데이트의 위험성에 대해 깊이 있게 공감한다. 데이터 요청이 발생 실패가 많이 발생하는 로직에 이를 도입하면 빠르고 자연스러운 UX를 위해 도입했다가 오히려 자연스럽지 않은 UX를 보이게 될 수도 있다… 데이터가 중요한 로직에 이를 적용하면 많은 리스크를 감당해야 될 수도 있다.
낙관적 업데이트 방식이 틀리다는 이야기는 절대 아니다!! 낙관적 업데이트도 프론트엔드 개발자라면 랜더링 방식을 생각할 때, 꼭 고민해야되는 내용이고, 상황에 따라 적용하는게 좋을수도 있고 아닐 수도 있다.
이는 낙관적 업데이트를 자주 경험해보고 적용해보면서 어느 상황에 적용해야 되는지 고민해보는 것이 중요한 것 같다.
이 블로그를 읽으시는 분들도 낙관적 업데이트에 대해 같이 고민해보는 시간이 되었으면 좋겠다 😃😃
프로젝트 코드가 궁금하시면 아래 Github가시면 Store구현 사항을 보실 수 있습니다.
좋은 글 잘 봤습니다! 클라이언트 개발에 대해 잘 모르다보니.. 낙관적 업데이트에 대해 처음 알게 되었네요.
말씀하신 "인스타그램 좋아요" 예시를 보면서, 서버의 낙관적 락(Optimissic Lock) 기법과 정말 유사한 동작원리인 것 같아서 더욱 공감되며 읽었던 것 같습니다. 쉬운 설명 때문에도 이해가 잘 된것같아요 👍
비관적 업데이트에 대한 내용도 더 자세한 내용이 있었으면 좋았을 것 같아요 ㅎㅎ (요즘 FE 에 대해 궁금한게 많다보니)