우리 브랜드,검색어별 점유율 관리하고 있나? 비전문가의 웹기반 tiny MVP 제작기
개발, 디자인에 대한 전문성 하나 없이 사이드프로젝트 처럼 아주 작은 웹기반 MVP를 만들어보기 위해 이것저것 시도해본 경험을 남긴다.
- 배경 부터 Ideation, 검증?
- '아주' 간단한 UI, UX 디자인
- 개발이랄 것까진 없는 Front-Backend 제작
- 로그분석과 홍보 등을 위한 Google Things 셋팅
의 순으로 설명해 보겠다. 결과는 아래와 같은 페이지.
0. 배경
해당 작업물은 PHP를 사용한 웹 기반으로 제작되었다. 전공도 아니고, 지식이 많지 않을 뿐더러, PHP는 2000년대 초중반에 잠깐 제로보드류들이 유행할 때 취미로 잠깐 공부해본 경험 밖에 없다.
평소에는 주로 파이썬, R, SQL을 이용해서 데이터 분석을 하고, 아래와 같이 개인적으로 필요한 자동화만 스크립트 기반으로 만들어보고 있다.
Facebook API 포스팅 가져오기 #1 API 사용
파이썬으로 인스타그램 포스팅
Excel에서 Syntax Highlighting SQL
문득 혼자만 쓰지말고, 같이 써보자는 생각에 자잘자잘한 주워들은 지식을 바탕으로 작은 MVP 서비스를 만들어 보기로 하였다.
1. 아이디어 발견
여러분의-주력-키워드는-검색점유율로-관리되고-있습니까
예전에 잠시 온라인 광고 업무를 맡았을 때, 위의 글을 본 적이 있어서 한 번 관리를 해봐야 겠다고 생각하고 있었는데, 다른 것들을 처리한다는 핑계와 업무 이동이 빨라지는 바람에 시도를 못하게 되었다.
보통 포털사이트에 마케팅을 한다고 하면, 검색광고를 제일 먼저 떠올리고 몇 순위에 위치하는지 신경을 쓴다. 해외의 경우 검색엔진최적화SEO 등도 중요하게 생각하지만, 우리나라의 경우 네이버의 높은 점유율과 포털의 특성으로 간과되는 경향이 있는 것 같다. 물론 광고가 중요한 부분임은 분명하다.
그런데 네이버에도 검색결과 중 광고부분 말고도 고객과의 접점이 많다는 것을 간과하는 것 같다. 블로그, 뉴스, 지식iN 등 여러부분에서도 브랜드를 노출시킬 수 있는 기회가 충분히 있는데도 말이다. 아래 이유들로 광고 외에 다른 부분도 신경을 써야 할 필요가 있다.
- 고객들은 광고보다는 리뷰에 더 신뢰를 가진다. 1)
- '단순노출의 효과'도 무시할 수 없다. 자꾸보면 친숙해 진다.
- 다른 사람들이 많이 언급하는 부분에 대한 군중심리가 작동한다. 평가 점수가 낮아도 리뷰 수가 많은 제품을 선호한다는 연구결과도 있다.2)
따라서 검색을 했을 때, 해당 페이지 내에서 우리의 브랜드가 얼마나 노출되는지, 경쟁 브랜드와 점유율은 어떤지 관리하는 것이 필요할 것이다.
SOV(Share Of Voice, 광고점유율, 특정 산업이나 분야에서 전체 광고 집행 비중에서 개별기업이 차지하는 광고의 비중을 의미한다.) 3) 라고도 할 수 있겠는데, 이를 확인해 볼 수 있는 페이지를 만들어 봐야 겠다고 생각했다.
2. 문제 검증하기
문제 정의
한마디로 표현하면 아래와 같다.
우리가 관리하는 검색어에서 “우리 브랜드의 노출 점유율”을 보고 싶다
기존 아이디어 탐색
- 이전에 봤다
- 과거 온라인 광고 업무를 하면서, 몇몇 업체에서 제공하는 솔루션에 해당 기능이 있는 것을 본 기억이 났다.
- 그래서 검색을 해봤다.
- 구글, 네이버에서 검색해 봤으나, 찾기가 힘들었다. 솔루션 들에서 제공하는 기능은 있지만, 웹 기반의 서비스는 없어보였다.
혹시 있으면 피드백 부탁드립니다.
고객 인터뷰하기
“해결하고자 하는 문제를 창업가 자신도 겪는가?”
기본적으로 내 경험을 통해, 내가 풀고 싶은 문제였기 때문에, 수요 조사는 크게 신경쓰지 않았지만, 주위 동료들도 나쁘지 않은 반응이었다.
하지만 다음의 문제는 있겠다.
가장 큰 이유
사이드프로젝트로 AtoZ까지 한 번 해보는 게 목적이었고, 비전문가인 나도 어렵지 않게 만들 수 있어 보였다는게 가장 큰이유였다.
3. MVP
Ideation
기본 기능은, 검색했을 때 브랜드 키워드의 노출 점유율을 보여준다
이며, 추가로 여러가지 기능들을 Ideation 해봤다.
Visualization
- 표
- 그래프로 점유율 표시
- 동적 분석
내용
- 검색 사이트별 : 네이버, 구글, 다음, 등
- 첫페이지 점유율
- Tab별 (광고, 웹페이지, 블로그, 뉴스 등) 점유율
- 1-depth 깊게 : 검색결과 클릭해서 나오는 페이지 내의 점유율까지 고려
기능
- 연관검색어 자동 검색
- 브랜드 유의어 (ex. 애플=Apple) 정의 기능
- 쿠키 등을 통해 입력값 기본 셋팅
- 엑셀 export
- 악의적 공격에 대한 방어 (ex. 봇bot)
- 트래픽이 몰렸을 때 분산
고도화
- 키워드의 긍정/부정 비율
- 해당 브랜드의 예상 '전체' 점유율 추정치
- 일별 Tracking
- 이미지나 동영상 콘텐츠
작게, 우선순위를 정하자
MVP 는 Minimum Viable Product의 줄임말로 초기 고객을 만족시키고 향후 제품 개발을위한 피드백을 제공하기에 충분한 기능을 갖춘 제품 버전을 말한다 4)
위의 여러가지 ideation 중에서 가장 Core로 생각할 수 있는 것만 만들어 보자.
1) 검색어 및 브랜드 키워드 입력
2) 첫 페이지에서 노출 수 집계
3) 검색사이트 중 점유율이 가장 높은 네이버
이다.
여기에 조금 더해서
1) 표로 점유율 표시
2) 부분별(광고, 블로그, 웹문서 등) 구분
3) 아주 조금의 UI/UX
를 추가해서 간단한 MVP를 제작해 보기로 했다.
4. UI/UX,
원페이지로 구성하고, input/output이 심플하여 아래 그림처럼 간단한 그림이 그려진다.
개발 내용은 아래 섹션에 있으며, 개발 후 몇가지 추가 보완을 하였다.
- 인터뷰결과 사이트의 기본 개념에 대한 이해가 낮아서 기본 입력값을 두도록 했다.
'에어컨'으로 검색했을 때 네이버에 나타나는 'LG,삼성,위니아, 캐리어' 브랜드별 점유율
- 기본 입력값이 있기 때문에 '초기화' 버튼 추가
- 조회에 시간이 걸릴 수 있고, 에러 사항을 표시해주기 위해, 사용자에게 '조회중/에러'를 표시하도록 구성
간단한 페이지에도 한없이 복잡해질 수 있기 때문에, '간단하고', '예상가능'하도록 하려고 노력했다.
기타디자인
디자인에는 정말 자신이 없기 때문에, reference에 있는 몇가지 참고 글에서 추천하는 내용을 적용하였다.
- 로고 ㅡ 간단히 파워포인트로 제작
- 나눔고딕 웹폰트를 사용하기 위해서 여기를 참고하였다.
- 완전한 검은색보다는 검은색에 가까운 색이 더 읽기 쉽다 : font-color → #333
- 흑백 형태로 먼저 디자인하고, 나중에 색상을 더한다.
- 색은 blue와 grey 투톤으로 했다
- 웹디자인이 밋밋하다면, 컬러가 있는 경계선을 사용해서 강조한다.
- 모든 버튼이 컬러를 가지고 있지 않아도 된다.
5. 개발
어떻게 만들까
내가 다룰 수 있는 툴과 현재 가용한 자원을 비교해서, PHP를 이용하여 개발하기로 했다.
- 다룰 수 있는 툴 : Python, PHP
- 가용한 자원 : PHP-mysql, 웹호스팅
- Python이 좀 더 익숙하고, 확장성이 높음
- Python을 사용하려면 Python 호스팅이 가능한 서버를 찾아야 함.
⇒ 웹호스팅을 사용하기 때문에, 설정 상 자유도에 한계가 있고, PHP의 기본 DOMdocumnet 클래스에서의 기능도 제한적이기 때문에(HTML5 지원이 잘 되지 않는다) 추후 서비스 확장, 고도화가 필요하면 다른 클라우드 서비스 및 Python 등으로 이관이 필요할 것 같다.
하지만, 지금의 자원 상에서 간단한 기능 구현에는 문제가 없을 것 같다.
Backend
이전에 만들었던 Facebook API 포스팅 가져오기 #4 PHP로 최신현황 받아오기 의 코드를 확장하면 크게 힘들이지 않고 만들 수 있을 것 같았다.
json 형식의 데이터를 받는 대신에, HTML 문서를 받아와서 적절히 파싱하면 되겠다.
HTML 문서 받아오기
function get_DOM($url) { #$content = json_decode(get_content($url)); $content = get_content($url); #echo strip_tags($content); $doc = new DOMDocument(); libxml_use_internal_errors(true); //원래는 @만 해도 되는데, error handler 넣어서 이것도 필요함 @$doc->loadHTML('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'.$content); //https://stackoverflow.com/questions/11819603/dom-loadhtml-doesnt-work-properly-on-a-server/11819635 Warning 없애기 @ //https://blog.serpongs.net/52 한글깨짐 return $doc; }
Tab별 글자수 세기
모바일 버전만 따로 보면, api_subject_bx 클래스를 가지는 div 태그를 찾으면 각 tab별로 내용을 구분 가능하다. 내용의 시작 '제목'을 판단해서 구분하면 된다.
function get_result_mo($doc, $keys) { $output_tmp = array(); $sections = ["파워링크","지식백과","지식iN", "VIEW", "이미지", "동영상", "뉴스", "플레이스", "앱정보", "네이버 책", "네이버쇼핑", "기획전", "국어사전", "결과 더보기"]; $xpath = new DOMXPath($doc); $tags = $xpath->query('//div[contains(@class, "api_subject_bx")]'); //section은 안됨 //https://stackoverflow.com/questions/1390568/how-can-i-match-on-an-attribute-that-contains-a-certain-string foreach ($tags as $tag) { $string=""; $string=trim($tag->nodeValue); $section = ""; for ($idx=0; $idx < count($sections); $idx++) { if ( preg_match("/^". $sections[$idx] ."/", $string) ) { $section = $sections[$idx]; break; } } if ($section == "결과 더보기") break; for ($idx2=0; $idx2 < count($keys); $idx2++) { $output_tmp[( $section =="" ? "웹문서": $section )][$keys[$idx2]] = substr_count($string, $keys[$idx2]); } } return $output_tmp; }
PHP 기본 DOMdocument 클래스에는 innerhtml 을 추출하는 기능이 없어, 아래의 예시를 사용하였다.
//https://www.php.net/manual/en/class.domelement.php function get_inner_html( $node ) { $innerHTML= ''; $children = $node->childNodes; foreach ($children as $child) { $innerHTML .= $child->ownerDocument->saveXML( $child ); } return $innerHTML; }
결과 출력
위의 함수들을 실행시키고, json 형식으로 출력하여 front-end로 결과값을 넘겨주게 된다.
$url = "https://m.search.naver.com/search.naver?sm=top_hty&fbm=1&ie=utf8&query=".urlencode($keyword); $doc = get_DOM($url); $output[$keyword]['Mo']['url'] = $url; $output[$keyword]['Mo']['data'] = get_result_mo($doc, $keys); $result = json_encode($output); echo $result;
로그
로그를 남겨서 사용현황을 살펴보아야 한다. 얼마나 많은 사람이 사용했는지, 왜 사용했는지 확인이 가능해야 계속 서비스를 운영할지 종료할지를 판단할 수 있기 때문이다. mysql에 간단한 로그 테이블을 생성해서 간단한 로그를 작성하도록 했다.
- 누가, 언제, 어디서, 무엇을, 어떻게, 왜
SQL injection 공격을 예방하기 위해서 prepare, bind_param 으로 미리 정해진 명령만 수행하도록 했다.
$dbConnect = new mysqli($host, $user, $pw, $dbName); $dbConnect->set_charset('utf8'); $dt = date_create()->format('Y-m-d H:i:s'); $ip = $_SERVER['REMOTE_ADDR']; $search_word = addslashes(str_replace(" ","+",$keyword)); $keys = addslashes(join("|",$keys)); $result_sql = addslashes(json_encode( $output, JSON_UNESCAPED_UNICODE )); //https://stackoverflow.com/questions/16498286/why-does-the-php-json-encode-function-convert-utf-8-strings-to-hexadecimal-entit $stmt = $dbConnect->prepare("INSERT INTO `log_sov` (dt,ip,search_word,`keys`,result_code,result) VALUES (?,?,?,?,?,?)"); $stmt->bind_param('ssssss', $dt,$ip,$search_word, $keys, $result_code, $result_sql); // 's' specifies the variable type => 'string' $stmt->execute();
에러의 경우에도 로그를 남겨야 하기 때문에, 에러 발생시 위의 로깅에 'error' code와 함께 에러 메시지를 로깅할 수 있도록 했다.
//https://stackoverflow.com/questions/2911094/outputting-all-php-errors-to-database-not-error-log function myErrorHandler($errno, $errstr, $errfile, $errline) { // you'd have to import or set up the connection here global $keyword, $keys; logging($keyword, $keys, "error",$errline." ".$errstr); exit(); } // set to the user defined error handler $old_error_handler = set_error_handler("myErrorHandler");
Frontend
AJAX
1페이지 내에서 구동하도록 만들고 싶었기 때문에, 조회하기 버튼을 눌렀을 때 AJAX 를 사용하여 위의 결과 json 값을 받아서 테이블을 구성하여 출력하도록 하였다.
$('#Btn_result').click( function() { // 넘겨줄 변수 var keyword = $("#keyword")[0].value var key = $("#keys")[0].value.trim() var keys = key.split(",").map(function(item) { return item.trim(); }); $.ajax({ beforeSend: function(){ // Handle the beforeSend event window.setTimeout(function() { console.log(""); }, 1000); }, type: 'post', dataType: 'json', url: 'sov.php', data: {'keyword':keyword, 'keys':keys}, async: true, success: function (data) { // JSON을 받는다 var obj = JSON.parse(JSON.stringify(data)); var device = ['Mo','PC']; for (var d in device) { var vals = []; for (var i in keys) vals[keys[i]]=0; out = ""; out = out + "<h3>"+(d==0?"모바일":"PC")+"</h3> <a href='"+data[keyword][device[d]]['url']+"' target=_blank>네이버링크</a><br>"; //https://getbootstrap.com/docs/3.4/css/#tables out = out + "<div class='table-responsive'><table class='table table-hover'>"; //https://github.com/twbs/bootstrap/issues/24638 out = out + "<thead><th style='min-width:100px'>구분</th>"; for (var k in keys) { out = out + "<th>" + keys[k] +"</th>"; } out = out + "</thead>"; // JSON을 table로 구성하는 부분 : 생략 out = out + "</tr> </table></div>"; $('#result'+d)[0].innerHTML = out; } window.setTimeout(function() { $('#result').hide(); }, 1000); }, error: function (request, status, error) { console.log(error); window.setTimeout(function() { $('#result_error').show(); }, 1000); } }); });
CSS
가장 많이 쓴다는 bootstrap 을 사용하기로 했다.
head 태그 안에 아래를 삽입하면 된다.
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
전문 프론트엔드개발자는 아니므로, 간단한 구조만 적용했고, 아래와 같다.
<body> <div class="container"> <header> 헤더부분 <ul class="list-inline"> <li>..</li> </ul> </header> <!-- Form, 입력부분 --> <div> <form> <div class="form-group"> <label>LABEL</label> <input class="form-control" type=text></input> </div> <button type="submit" class="btn btn-default">조회하기</button> </form> </div> <!-- 결과 출력 부분 --> <div> <div class="row"> <div id='result0' class="col-md-6 result"><br></div> <div id='result1' class="col-md-6 result"><br></div> </div> </div> </footer>푸터부분</footer> </div> </body>
- 기본적으로 container 클래스 안에 내용을 넣는다
- 헤더부분
- ul-li 리스트들은 list-inline 클래스를 사용하면 한줄로 정렬된다.
- form 객체들은 form-group 클래스로 묶고, 각각 form-control 클래스를 적용한다
- 버튼은 btn 및 btn-default 모양을 준다
- 결과출력부분
- row 클래스로 묶으면 device의 넓이에 따라 적절히 가로로 배열해 준다
- 총 12의 넓이로 col-md-6 으로 반반씩 나눠주었다
- (AJAX부분)
- table은 기본적으로 table 클래스를 적용
- table-hover 로 설정하면 마우스over 할 때 highlight가 된다.
- table-responsive 로 하면 테이블 크기가 클 때 scroll-bar를 생성해준다
5. 기타 신경써야 할 것들
Google Analytics
google analytics 홈페이지에서 로그인 > 계정생성 > 홈페이지를 등록한 후 나오는 스크립트를 바로 페이지에 삽입하면 된다.
또한 속성 옵션에서 Search Console 에 연결하면, 구글 검색 Optimization을 위한 준비가 완료된다.
Google Adsense
adsense 페이지에서 홈페이지를 등록한 후 ad 스크립트를 바로 페이지에 삽입하면 된다.
자동 반응형으로 설정하는 경우 페이지 아무 곳에서 넣으면 되고, 따로 원하는 위치가 있을 경우 원하는 위치에 넣도록 하자.
Google Tagmanager
GA 이벤트들을 쉽게 관리하기 위해서 GTM 사이트에서 계정 및 컨테이너를 생성한 후 나오는 스크립트를 페이지에 삽입한다.
여기서 조회버튼 클릭시 검색어와 키워드들을 GA 이벤트로 등록하도록 설정한다.
- trigger : id 'Btn_result' 클릭
- variable : custom javascript : '검색어'+'/'+'비교키워드'
네이버 웹마스터
네이버 검색에도 나올 수 있도록 등록해야 한다
https://searchadvisor.naver.com/start
주의사항
Toy example 이기 때문에, 데이터가 정확하지 않다는 주의문구는 삽입이 필요하겠다.
피드백
구글 설문지 홈페이지 를 통해서 보완점이나 오류 사항 등 피드백을 받을 수 있도록 했다.
홍보
이제 홍보를 시작하고, 사용량과 피드백을 바탕으로 서비스를 고도화할지, 종료할지 정하면 되겠다.
마무리
- 내가 혼자 사용할 때는 신경쓰지 않아도 될 여러가지가 있어서 생각보다 시간이 오래 걸렸다.
- 심플하게 만들고자 했지만, 더 심플한 방법이 존재할 수도 있겠다.
—-
사이트 제작방법이든, 사이트 이용이든 오류나 제안사항에 대한 많은 피드백 부탁드립니다.
Reference
창업의 과학, 다도코로 마사유키, 한빛미디어, 2019
Bootstrap Documents
웹코딩 시작하기, 김태영, 정보문화사, 2017
디자이너가 아닌 당신에게 꼭 필요한 5분 디자인 가이드
디자인을 전문적으로 몰라도, 그럴싸하게 쓸 수 있는 웹디자인 TIP
More
2021/09/24 15:30 | |
2020/06/18 20:34 | |
2020/06/07 23:03 | |
2020/06/04 00:30 | |
2020/05/28 19:29 | |
2020/05/12 18:03 | |
2020/05/07 18:27 | |
2020/05/01 01:54 | |
2020/04/29 19:52 | |
2020/04/20 20:12 | |
2020/04/17 00:23 | |
2020/04/15 16:21 | |
2020/04/11 10:41 | |
2020/04/08 15:49 | |
2020/04/01 18:43 | |
2020/04/01 09:55 | |
2020/03/21 22:43 | |
2020/03/20 20:53 | |
2020/03/15 20:11 | |
2020/03/07 16:35 | |
2020/03/07 13:46 | |
2020/03/02 01:09 | |
2020/02/29 13:52 | |
2020/02/29 13:52 | |
2020/02/29 13:52 | |
2020/02/29 13:52 | |
2020/02/29 13:52 | |
2020/02/29 13:49 | |
2020/02/16 18:11 | |
2020/02/06 21:58 |
Discussion