티스토리 뷰

여러 카테고리의 앱들을 만들어보면 각 앱들의 카테고리마다 공통으로, 자주 쓰는 기능들이 존재한다.

대표적으로 SNS 앱에서는 친구 관계에 대한 기능을 예로 들 수 있다.

 

팔로우, 친구 등 이름으로 쓰이는 친구 관계는 생각보다 나름(?) 복잡한 관계를 가지고 있다. 

친구 신청중, 친구 요청 받음, 친구, 차단됨, 내가 차단 시킴 ... 등 여러가지 복잡한 상태를 가지고 있으며, 이것 또한 앱마다 다를 것이다.

 

따라서 어떠한 방식이든 클라이언트와 서버간 relationship에 대한 약속이 필요하며, 이는 하나의 앱에서 동일한 작동을 보장해야 한다.

 

이번에 개발에 참여한 SNS 앱에서도 당연하게도 친구 관계에 대한 UI가 많이 있었다.

비슷한 기능의 친구 Action View, 실제로는 더 많은 곳에서 사용된다.

이런 여러가지 뷰에서 사용되는 동일한 기능들을 각각의 뷰에서 하나하나 구현한다면 이런 문제점이 있다.

1. 동일한 코드를 계속 복붙해야 하고

2. 수정이 하나라도 생기면 여러 코드를 찾아가며 고쳐야 하고

3. 하나하나 구현하거나 복붙하는 과정에서 실수가 있을 수 있고

4. 멋있지 않다

 

따라서, 이 프로젝트에서는 친구 관계 액션에 대한 공통 기능들과 뷰를 묶어서, 어느곳에서든 꺼내서 쓸 수 있는 뷰를 만들어 사용했다.

 

1. Relationship

우선 친구에 대해 어떤 액션(친구 신청 등)을 하려면, 현재 상태에 대한 정의가 있어야 한다.

서버에서는 친구 정보를 내려줄 때 이를 relationship 이라는 정수형 값으로 함께 내려주기로 했다.

나는 내려주는 정수형을 Relationship 이라는 enum의 rawValue로 받아 변환했다.

enum Relationship: Int {
    /// 0. 차단한 상태
    case block = 0
    /// 1. 내가 친구요청을 걸음
    case friendRequested = 1
    /// 2. 상대방에게 친구 요청을 받음
    case friendRequestReceived = 2
    /// 3. 친구
    case friend = 3
    /// 4. 친구 거절당함
    case friendRejected = 4
    /// 5. 아무 관계도 아님
    case none = 5
    /// 6. 나 자신
    case `self` = 6
}

서버에서는 아래 예시와 같이 user의 데이터를 내려주며, 이와 함께 relationship 을 보내준다.

struct ProfileResults: Codable {
    let userId: Int
    var id: String
    var userName: String
    let point: Int
    let relationship: Int? -> 서버에서 내려주는 relationship
    let imageUrls: ProfileImageUrls
}

MVVM 패턴을 사용한 본 프로젝트에서는 비즈니스 로직을 담당하는 뷰모델이 유저의 프로필 정보를 가지고 있도록 했다.

뷰모델에서는 저장 프로퍼티(stored property)로 프로필 정보를 가지고 있으면서,

연산 프로퍼티(computed property)로 relationship을 리턴하는 변수를 만들었다.

relationship을 profile에서 이미 저장하고 있기 때문에 중복 저장을 방지하면서,

액션 이후 relationship이 변경되더라도, 변한 관계에 대한 설정을 가져올 것이다.

class ProfileViewModel: ViewModelType {
	...
// 프로필 정보 저장
let profile = BehaviorRelay<ProfileModel?>(value: nil)

		// relationship
    var relationShip: Relationship {
        guard let relationship = profile.value?.results?.relationship else { return .none }
        return Relationship(rawValue: relationship) ?? .none
    }
	...
}

만~약 relationship에 약속에 없던 엉뚱한 값이 들어온다면, nil이 될 것이고, 그에 따른 기본값은 아무 관계가 없는 사이로 만들었다.

 

 

2. Relationship의 속성

디자인에 따르면 유저와 유저가 조회한 타 유저의 relationship 상태에 따라 버튼의 속성값들이 달라진다. 

(버튼 배경색, 텍스트, 글자색을 포함하여 호출하는 API까지)

 

이러한 것을 모두 하나하나 구현하면 관리도 힘들고 알아보기도 힘들기 때문에 만들어둔 Relationship 열거형 내부에 값을 넣었다.

 

enum Relationship: Int {
    /// 0. 차단한 상태
    case block = 0
    /// 1. 내가 친구요청을 걸음
    case friendRequested = 1
    /// 2. 상대방에게 친구 요청을 받음
    case friendRequestReceived = 2
    /// 3. 친구
    case friend = 3
    /// 4. 친구 거절당함
    case friendRejected = 4
    /// 5. 아무 관계도 아님
    case none = 5
    /// 6. 나 자신
    case `self` = 6
    
    case requestRejectConfig = 88 // 친구요청 거절
    
    // 버튼 배경색
    var backgroundColor: UIColor {
        switch self {
        case .friendRequestReceived, .block, .none, .friendRejected:
            return .pointerRed
        case .friend, .requestRejectConfig, .friendRequested, .`self`:
            return .navBackColor
        }
    }
    
    var tintColor: UIColor {
        return .white
    }
    
    // attribute Title
    var attributedTitle: NSAttributedString {
        switch self {
        case .block:
            return getButtonTitle(title: "차단 해제")
        case .friendRequested:
            return getButtonTitle(title: "요청 취소")
        case .friendRequestReceived:
            return getButtonTitle(title: "요청 수락")
        case .friend:
            return getButtonTitle(title: "친구 ✓")
        case .friendRejected, .none:
            return getButtonTitle(title: "친구 신청")
        case .requestRejectConfig:
            return getButtonTitle(title: "거절")
        case .`self`:
            return getButtonTitle(title: "나")
        }
    }
    
    var smallAttributedTitle: NSAttributedString {
        switch self {
        case .block:
            return getButtonTitle(title: "차단 해제", size: 11)
        case .friendRequested:
            return getButtonTitle(title: "요청 취소", size: 11)
        case .friendRequestReceived:
            return getButtonTitle(title: "요청 수락", size: 11)
        case .friend:
            return getButtonTitle(title: "친구 ✓", size: 11)
        case .friendRejected, .none:
            return getButtonTitle(title: "친구 신청", size: 11)
        case .requestRejectConfig:
            return getButtonTitle(title: "거절", size: 11)
        case .`self`:
            return getButtonTitle(title: "나", size: 11)
        }
    }
    
    // alert title
    var alertTitle: String {
        switch self {
        case .block: return "차단 해제"
        case .friendRequested: return "요청 취소"
        case .friendRequestReceived: return "요청 수락"
        case .friend: return "친구 해제"
        case .friendRejected, .none: return "친구 요청"
        case .requestRejectConfig: return "요청 거절"
        case .`self`: return ""
        }
    }
    
    // alert title
    var alertActionTitle: String {
        switch self {
        case .block: return "해제"
        case .friendRequested: return "확인"
        case .friendRequestReceived: return "수락"
        case .friend: return "해제"
        case .friendRejected, .none: return "요청"
        case .requestRejectConfig: return "거절"
        case .`self`: return ""
        }
    }
    
    // alert 메시지
    func getAlertMessage(targetName: String?, targetId: String?) -> String {
        
        let targetName = targetName ?? ""
        let targetId = targetId ?? ""
        
        switch self {
        case .block: return "\(targetName)(\(targetId))님의 차단을 해제하시겠어요??"
        case .friendRequested: return "친구 요청을\n취소하시겠어요?"
        case .friendRequestReceived: return "\(targetName)(\(targetId))님의 친구 요청을\n수락하시겠어요?"
        case .friend: return "\(targetName)(\(targetId))님과\n친구를 해제하시겠어요?"
        case .friendRejected, .none: return "\(targetName)(\(targetId))님에게 친구를 요청하시겠어요?"
        case .requestRejectConfig: return "\(targetName)(\(targetId))님의 요청을 거절하시겠어요?"
        case .`self`: return ""
        }
    }
    
    // 네트워크 요청 라우터
    var router: FriendRouter? {
        switch self {
        case .block: return .cancelBlockFriend
        case .friendRequested: return .cancelRequestFriend
        case .friendRequestReceived: return .acceptFreindRequest
        case .friend: return .breakFreind
        case .friendRejected, .none: return .requestFriend
        case .requestRejectConfig: return .rejectFriendRequest
        case .`self`: return nil
        }
    }
    
    // 버튼 Attributed Title
    func getButtonTitle(title: String, size: Int = 13) -> NSAttributedString {
        let string = NSAttributedString(string: title, attributes: [NSAttributedString.Key.font: UIFont.notoSans(font: .notoSansKrMedium, size: CGFloat(size))])
        return string
    }
}

 

코드가 좀 길긴 하지만 앱 내에서 사용되는 모든 친구 관계와 그에 따른 뷰와 액션의 속성은 모두 위의 코드에서 결정되도록 했다. 

따라서, 친구 관계에 관련된 뷰 등을 수정하기 위해서는 위 코드만 들여다 보면 된다.

 

위 코드에는 해당 버튼을 탭했을 때 한번 더 체크하는 AlertView의 메시지도 담아두었다.

 

이제 아래 그림과 같이 UI에서는 하나의 버튼만 구현하고, 그 속성값을 Relationship에서 전달받아 화면에 뿌려주게 된다.

앱 내에서의 모든 친구 관계와 액션은 이러한 flow로 구성된다

 

아래 코드는 프로필 보기 뷰에서 버튼 UI를 뿌리는 부분이다.

    // 유저 타입별 분기 처리
    private func configureActionButtonUI(model: ProfileModel) {
        guard let viewModel = viewModel else { return }
        
        // friendActionButton 을 추가
        buttonStack.addArrangedSubview(friendActionButton)
        
        // friendActionButton의 세부 옵션
        friendActionButton.tintColor = viewModel.relationShip.tintColor
        friendActionButton.backgroundColor = viewModel.relationShip.backgroundColor
        friendActionButton.setAttributedTitle(viewModel.relationShip.attributedTitle, for: .normal)
        
        // message 버튼도 추가
        buttonStack.addArrangedSubview(messageButton)

        // 버튼 Corner Radius
        buttonStack.subviews.forEach {
            $0.layer.cornerRadius = 28 / 2
            $0.clipsToBounds = true
            $0.widthAnchor.constraint(equalToConstant: 80).isActive = true
        }
    }

 

이 외에도 Relationship을 생성자로 받는 커스텀 뷰를 만들어 버튼 UI를 구현해두기도 했다.

 

3. Router

Router는 http통신에 필요한 url, path, method, header, parameter 등을 모아놓은 열거형이다.

Relationship의 상태에 따라 수행될 다음 액션에 대한 네트워크를 Router에 구현해두었고

버튼 탭 액션은 router를 네트워크 Request 함수에 전달해 해당 통신을 수행하게 된다.

 

--

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함